diff --git a/src/repositories/InMemoryParkingRepository.ts b/src/repositories/InMemoryParkingRepository.ts index f5c63df..e162dcb 100644 --- a/src/repositories/InMemoryParkingRepository.ts +++ b/src/repositories/InMemoryParkingRepository.ts @@ -3,7 +3,7 @@ import { IParkingStructure, IParkingStructureTimestampRecord } from "../entities/ParkingRepositoryEntities"; -import { HistoricalParkingAverageQueryResult } from "./ParkingGetterRepository"; +import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository"; import { CircularQueue } from "../types/CircularQueue"; export type ParkingStructureID = string; @@ -74,7 +74,61 @@ export class InMemoryParkingRepository implements ParkingGetterSetterRepository return null; }; - getHistoricalAveragesOfParkingStructureCounts = async (id: string): Promise => { - return []; + getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise => { + 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, + 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 + }; }; } diff --git a/src/repositories/ParkingGetterRepository.ts b/src/repositories/ParkingGetterRepository.ts index c098c0e..8ef13c9 100644 --- a/src/repositories/ParkingGetterRepository.ts +++ b/src/repositories/ParkingGetterRepository.ts @@ -9,7 +9,7 @@ export interface ParkingStructureCountOptions { export interface HistoricalParkingAverageQueryResult { fromUnixEpochMs: number; toUnixEpochMs: number; - averageSpotsTaken: number; + averageSpotsAvailable: number; } diff --git a/test/repositories/InMemoryParkingRepositoryTests.test.ts b/test/repositories/InMemoryParkingRepositoryTests.test.ts index 24c292f..ff74c68 100644 --- a/test/repositories/InMemoryParkingRepositoryTests.test.ts +++ b/test/repositories/InMemoryParkingRepositoryTests.test.ts @@ -5,6 +5,7 @@ import { } from "../../src/repositories/InMemoryParkingRepository"; import { IParkingStructure, IParkingStructureTimestampRecord } from "../../src/entities/ParkingRepositoryEntities"; import { CircularQueue } from "../../src/types/CircularQueue"; +import { ParkingStructureCountOptions } from "../../src/repositories/ParkingGetterRepository"; describe("InMemoryParkingRepository", () => { let repository: InMemoryParkingRepository; @@ -145,4 +146,112 @@ describe("InMemoryParkingRepository", () => { expect(result).toEqual(testStructure); }); }); + + describe("getHistoricalAveragesOfParkingStructureCounts", () => { + const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs - b.timestampMs; + + const setupHistoricalData = (records: IParkingStructureTimestampRecord[]) => { + const queue = new CircularQueue(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 + }); + }); + }); });