mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Implement getHistoricalAveragesOfParkingStructureCounts for InMemoryParkingRepository.ts, and add tests
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
|||||||
IParkingStructure,
|
IParkingStructure,
|
||||||
IParkingStructureTimestampRecord
|
IParkingStructureTimestampRecord
|
||||||
} from "../entities/ParkingRepositoryEntities";
|
} from "../entities/ParkingRepositoryEntities";
|
||||||
import { HistoricalParkingAverageQueryResult } from "./ParkingGetterRepository";
|
import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository";
|
||||||
import { CircularQueue } from "../types/CircularQueue";
|
import { CircularQueue } from "../types/CircularQueue";
|
||||||
|
|
||||||
export type ParkingStructureID = string;
|
export type ParkingStructureID = string;
|
||||||
@@ -74,7 +74,61 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
getHistoricalAveragesOfParkingStructureCounts = async (id: string): Promise<HistoricalParkingAverageQueryResult[]> => {
|
getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise<HistoricalParkingAverageQueryResult[]> => {
|
||||||
return [];
|
const queue = this.historicalData.get(id);
|
||||||
|
if (!queue || queue.size() === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: HistoricalParkingAverageQueryResult[] = [];
|
||||||
|
const { startUnixEpochMs, endUnixEpochMs, intervalMs } = options;
|
||||||
|
|
||||||
|
let currentIntervalStart = startUnixEpochMs;
|
||||||
|
|
||||||
|
while (currentIntervalStart < endUnixEpochMs) {
|
||||||
|
const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endUnixEpochMs);
|
||||||
|
const recordsInInterval = this.getRecordsInTimeRange(queue, currentIntervalStart, currentIntervalEnd);
|
||||||
|
|
||||||
|
if (recordsInInterval.length > 0) {
|
||||||
|
const averageResult = this.calculateAverageForInterval(currentIntervalStart, currentIntervalEnd, recordsInInterval);
|
||||||
|
results.push(averageResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIntervalStart = currentIntervalEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getRecordsInTimeRange = (
|
||||||
|
queue: CircularQueue<IParkingStructureTimestampRecord>,
|
||||||
|
startMs: number,
|
||||||
|
endMs: number
|
||||||
|
): IParkingStructureTimestampRecord[] => {
|
||||||
|
const recordsInInterval: IParkingStructureTimestampRecord[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < queue.size(); i++) {
|
||||||
|
const record = queue.get(i);
|
||||||
|
if (record && record.timestampMs >= startMs && record.timestampMs < endMs) {
|
||||||
|
recordsInInterval.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordsInInterval;
|
||||||
|
};
|
||||||
|
|
||||||
|
private calculateAverageForInterval = (
|
||||||
|
fromMs: number,
|
||||||
|
toMs: number,
|
||||||
|
records: IParkingStructureTimestampRecord[]
|
||||||
|
): HistoricalParkingAverageQueryResult => {
|
||||||
|
const totalSpotsAvailable = records.reduce((sum, record) => sum + record.spotsAvailable, 0);
|
||||||
|
const averageSpotsAvailable = totalSpotsAvailable / records.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fromUnixEpochMs: fromMs,
|
||||||
|
toUnixEpochMs: toMs,
|
||||||
|
averageSpotsAvailable
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface ParkingStructureCountOptions {
|
|||||||
export interface HistoricalParkingAverageQueryResult {
|
export interface HistoricalParkingAverageQueryResult {
|
||||||
fromUnixEpochMs: number;
|
fromUnixEpochMs: number;
|
||||||
toUnixEpochMs: number;
|
toUnixEpochMs: number;
|
||||||
averageSpotsTaken: number;
|
averageSpotsAvailable: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "../../src/repositories/InMemoryParkingRepository";
|
} from "../../src/repositories/InMemoryParkingRepository";
|
||||||
import { IParkingStructure, IParkingStructureTimestampRecord } from "../../src/entities/ParkingRepositoryEntities";
|
import { IParkingStructure, IParkingStructureTimestampRecord } from "../../src/entities/ParkingRepositoryEntities";
|
||||||
import { CircularQueue } from "../../src/types/CircularQueue";
|
import { CircularQueue } from "../../src/types/CircularQueue";
|
||||||
|
import { ParkingStructureCountOptions } from "../../src/repositories/ParkingGetterRepository";
|
||||||
|
|
||||||
describe("InMemoryParkingRepository", () => {
|
describe("InMemoryParkingRepository", () => {
|
||||||
let repository: InMemoryParkingRepository;
|
let repository: InMemoryParkingRepository;
|
||||||
@@ -145,4 +146,112 @@ describe("InMemoryParkingRepository", () => {
|
|||||||
expect(result).toEqual(testStructure);
|
expect(result).toEqual(testStructure);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getHistoricalAveragesOfParkingStructureCounts", () => {
|
||||||
|
const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs - b.timestampMs;
|
||||||
|
|
||||||
|
const setupHistoricalData = (records: IParkingStructureTimestampRecord[]) => {
|
||||||
|
const queue = new CircularQueue<IParkingStructureTimestampRecord>(10);
|
||||||
|
records.forEach(record => queue.appendWithSorting(record, sortingCallback));
|
||||||
|
historicalData.set(testStructure.id, queue);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return empty array for non-existent structure or no data", async () => {
|
||||||
|
const options: ParkingStructureCountOptions = {
|
||||||
|
startUnixEpochMs: 1000,
|
||||||
|
endUnixEpochMs: 2000,
|
||||||
|
intervalMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
// Non-existent structure
|
||||||
|
expect(await repository.getHistoricalAveragesOfParkingStructureCounts("non-existent", options)).toEqual([]);
|
||||||
|
|
||||||
|
// Structure with no historical data
|
||||||
|
await repository.addOrUpdateParkingStructure(testStructure);
|
||||||
|
expect(await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should calculate averages for single and multiple intervals", async () => {
|
||||||
|
const records = [
|
||||||
|
{ id: testStructure.id, spotsAvailable: 80, timestampMs: 1100 },
|
||||||
|
{ id: testStructure.id, spotsAvailable: 70, timestampMs: 1200 },
|
||||||
|
{ id: testStructure.id, spotsAvailable: 50, timestampMs: 1600 },
|
||||||
|
{ id: testStructure.id, spotsAvailable: 40, timestampMs: 1700 }
|
||||||
|
];
|
||||||
|
|
||||||
|
setupHistoricalData(records);
|
||||||
|
await repository.addOrUpdateParkingStructure(testStructure);
|
||||||
|
|
||||||
|
// Single interval test
|
||||||
|
const singleIntervalOptions: ParkingStructureCountOptions = {
|
||||||
|
startUnixEpochMs: 1000,
|
||||||
|
endUnixEpochMs: 1500,
|
||||||
|
intervalMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleResult = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, singleIntervalOptions);
|
||||||
|
expect(singleResult).toHaveLength(1);
|
||||||
|
expect(singleResult[0]).toEqual({
|
||||||
|
fromUnixEpochMs: 1000,
|
||||||
|
toUnixEpochMs: 1500,
|
||||||
|
averageSpotsAvailable: 75 // (80 + 70) / 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Multiple intervals test
|
||||||
|
const multipleIntervalOptions: ParkingStructureCountOptions = {
|
||||||
|
startUnixEpochMs: 1000,
|
||||||
|
endUnixEpochMs: 2000,
|
||||||
|
intervalMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const multipleResult = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, multipleIntervalOptions);
|
||||||
|
expect(multipleResult).toHaveLength(2);
|
||||||
|
expect(multipleResult[0]).toEqual({
|
||||||
|
fromUnixEpochMs: 1000,
|
||||||
|
toUnixEpochMs: 1500,
|
||||||
|
averageSpotsAvailable: 75 // (80 + 70) / 2
|
||||||
|
});
|
||||||
|
expect(multipleResult[1]).toEqual({
|
||||||
|
fromUnixEpochMs: 1500,
|
||||||
|
toUnixEpochMs: 2000,
|
||||||
|
averageSpotsAvailable: 45 // (50 + 40) / 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge cases: skipped intervals and boundaries", async () => {
|
||||||
|
const records = [
|
||||||
|
{ id: testStructure.id, spotsAvailable: 90, timestampMs: 1000 }, // start boundary - included
|
||||||
|
{ id: testStructure.id, spotsAvailable: 80, timestampMs: 1500 }, // interval boundary - included in second
|
||||||
|
{ id: testStructure.id, spotsAvailable: 60, timestampMs: 2100 } // skip interval, in third
|
||||||
|
];
|
||||||
|
|
||||||
|
setupHistoricalData(records);
|
||||||
|
await repository.addOrUpdateParkingStructure(testStructure);
|
||||||
|
|
||||||
|
const options: ParkingStructureCountOptions = {
|
||||||
|
startUnixEpochMs: 1000,
|
||||||
|
endUnixEpochMs: 2500,
|
||||||
|
intervalMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await repository.getHistoricalAveragesOfParkingStructureCounts(testStructure.id, options);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
fromUnixEpochMs: 1000,
|
||||||
|
toUnixEpochMs: 1500,
|
||||||
|
averageSpotsAvailable: 90 // only record at 1000ms
|
||||||
|
});
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
fromUnixEpochMs: 1500,
|
||||||
|
toUnixEpochMs: 2000,
|
||||||
|
averageSpotsAvailable: 80 // only record at 1500ms
|
||||||
|
});
|
||||||
|
expect(result[2]).toEqual({
|
||||||
|
fromUnixEpochMs: 2000,
|
||||||
|
toUnixEpochMs: 2500,
|
||||||
|
averageSpotsAvailable: 60 // only record at 2100ms
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user