diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index a18ca58..930b336 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -117,7 +117,7 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette private createEtaKey = (shuttleId: string, stopId: string) => `shuttle:eta:${shuttleId}:${stopId}`; private createOrderedStopKey = (routeId: string, stopId: string) => `shuttle:orderedstop:${routeId}:${stopId}`; private createShuttleLastStopKey = (shuttleId: string) => `shuttle:laststop:${shuttleId}`; - private createHistoricalEtaTimeSeriesKey =(routeId: string, fromStopId: string, toStopId: string) => { + private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`; } @@ -446,7 +446,7 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStopArrival.stopId); const nextStop = lastOrderedStop?.nextStop; if (nextStop == null) return; - + const travelTimeSeconds = await this.getAverageTravelTimeSeconds({ routeId: shuttle.routeId, fromStopId: lastStopArrival.stopId, @@ -500,7 +500,7 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette const timeSeriesKey = this.createHistoricalEtaTimeSeriesKey(routeId, fromStopId, toStopId); const fromTimestamp = from.getTime(); const toTimestamp = to.getTime(); - const intervalMs = toTimestamp - fromTimestamp; + const intervalMs = toTimestamp - fromTimestamp + 1; try { const aggregationResult = await this.redisClient.sendCommand([ @@ -518,14 +518,14 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette return parseFloat(averageValue); } - throw new Error(`No historical data found for route ${routeId} from stop ${fromStopId} to stop ${toStopId}`); + return; } catch (error) { console.warn(`Failed to get average travel time: ${error instanceof Error ? error.message : String(error)}`); return; } } - private async addTravelTimeDataPoint( + public async addTravelTimeDataPoint( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, travelTimeSeconds: number, timestamp = Date.now(), @@ -634,7 +634,7 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette }); } - public async addOrUpdateStop(stop: IStop): Promise { + public async addOrUpdateStop(stop: IStop): Promise { const key = this.createStopKey(stop.id); await this.redisClient.hSet(key, this.createRedisHashFromStop(stop)); } diff --git a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts b/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts index 9cb2220..11314e2 100644 --- a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts +++ b/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts @@ -253,4 +253,113 @@ describe("RedisShuttleRepository", () => { expect(result?.timestamp.getTime()).toBe(secondArrival.timestamp.getTime()); }); }); + + describe("getAverageTravelTimeSeconds", () => { + it("returns the average travel time when historical data exists", async () => { + const { route, stop2, stop1 } = await setupRouteAndOrderedStops(); + + // Add a single data point: 15 minutes travel time + const timestamp = new Date(2025, 0, 1, 12, 15, 0); + await repository.addTravelTimeDataPoint( + { + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, + 15 * 60, // 15 minutes in seconds + timestamp.getTime() + ); + + const travelTime = await repository.getAverageTravelTimeSeconds({ + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, { + from: new Date(2025, 0, 1, 11, 0, 0), + to: new Date(2025, 0, 1, 13, 0, 0), + }); + + expect(travelTime).toEqual(15 * 60); + }); + + it("returns average of multiple data points", async () => { + const { route, stop2, stop1 } = await setupRouteAndOrderedStops(); + + // First trip: 10 minutes travel time + await repository.addTravelTimeDataPoint( + { + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, + 10 * 60, // 10 minutes + new Date(2025, 0, 1, 12, 0, 0).getTime() + ); + + // Second trip: 20 minutes travel time + await repository.addTravelTimeDataPoint( + { + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, + 20 * 60, // 20 minutes + new Date(2025, 0, 1, 13, 0, 0).getTime() + ); + + const averageTravelTime = await repository.getAverageTravelTimeSeconds({ + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, { + from: new Date(2025, 0, 1, 11, 0, 0), + to: new Date(2025, 0, 1, 14, 0, 0), + }); + + // Average of 10 minutes and 20 minutes = 15 minutes = 900 seconds + expect(averageTravelTime).toEqual(15 * 60); + }); + + it("returns undefined when no data exists", async () => { + const { route, stop1, stop2 } = await setupRouteAndOrderedStops(); + + // Don't add any data points, just query for data that doesn't exist + const averageTravelTime = await repository.getAverageTravelTimeSeconds({ + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, { + from: new Date(2025, 0, 1, 11, 0, 0), + to: new Date(2025, 0, 1, 14, 0, 0), + }); + + expect(averageTravelTime).toBeUndefined(); + }); + + it("returns undefined when querying outside the time range of data", async () => { + const { route, stop2, stop1 } = await setupRouteAndOrderedStops(); + + // Add data on Jan 1 + await repository.addTravelTimeDataPoint( + { + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, + 15 * 60, // 15 minutes + new Date(2025, 0, 1, 12, 15, 0).getTime() + ); + + // Query for Jan 2 (no data should exist in this range) + const averageTravelTime = await repository.getAverageTravelTimeSeconds({ + routeId: route.id, + fromStopId: stop1.id, + toStopId: stop2.id, + }, { + from: new Date(2025, 0, 2, 11, 0, 0), + to: new Date(2025, 0, 2, 13, 0, 0), + }); + expect(averageTravelTime).toBeUndefined(); + }); + }); });