From 91517669f0f74d5d7bb1d8afbac7e49197a516c5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 10 Nov 2025 18:11:54 -0800 Subject: [PATCH] Implement updateEtasBasedOnHistoricalData and add a test for it --- .../shuttle/RedisShuttleRepository.ts | 44 ++++++++++++++---- .../__tests__/RedisShuttleRepository.test.ts | 45 +++++++++++++++++-- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 2962975..f00819e 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -425,21 +425,48 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette public async addOrUpdateShuttle( shuttle: IShuttle, travelTimeTimestamp = Date.now(), + referenceCurrentTime = new Date(), ): Promise { const key = this.createShuttleKey(shuttle.id); await this.redisClient.hSet(key, this.createRedisHashFromShuttle(shuttle)); await this.updateHistoricalEtasForShuttle(shuttle, travelTimeTimestamp); - await this.updateEtasBasedOnHistoricalData(shuttle); + await this.updateEtasBasedOnHistoricalData(shuttle, referenceCurrentTime); } - public async updateEtasBasedOnHistoricalData( + private async updateEtasBasedOnHistoricalData( shuttle: IShuttle, + referenceCurrentTime: Date = new Date(), ) { - // Based on historical data for the key provided by this.createHistoricalEtaTimeSeriesKey(routeId, fromStopId, toStopId); - // Call this.addOrUpdateEta with the correct information - // "Historical data" being averaged time taken for the current hour of the same day - // in the past week, based on the reference time + const oneWeekAgo = new Date(referenceCurrentTime.getTime() - (60 * 60 * 24 * 7 * 1000)) + + const lastStopArrival = await this.getShuttleLastStopArrival(shuttle) + if (lastStopArrival == undefined) return; + + 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, + toStopId: nextStop.stopId, + }, { + from: oneWeekAgo, + to: new Date(oneWeekAgo.getTime() + (60 * 60 * 1000)) + }); + if (travelTimeSeconds == undefined) return; + + const elapsedTimeMs = referenceCurrentTime.getTime() - lastStopArrival.timestamp.getTime(); + const predictedTimeSeconds = travelTimeSeconds - (elapsedTimeMs / 1000); + + await this.addOrUpdateEta({ + secondsRemaining: predictedTimeSeconds, + shuttleId: shuttle.id, + stopId: nextStop.stopId, + systemId: nextStop.systemId, + updatedTime: new Date(), + }); } private async updateHistoricalEtasForShuttle( @@ -469,7 +496,7 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette public async getAverageTravelTimeSeconds( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, { from, to }: ShuttleTravelTimeDateFilterArguments, - ): Promise { + ): Promise { const timeSeriesKey = this.createHistoricalEtaTimeSeriesKey(routeId, fromStopId, toStopId); const fromTimestamp = from.getTime(); const toTimestamp = to.getTime(); @@ -493,7 +520,8 @@ export class RedisShuttleRepository extends EventEmitter implements ShuttleGette throw new Error(`No historical data found for route ${routeId} from stop ${fromStopId} to stop ${toStopId}`); } catch (error) { - throw new Error(`Failed to get average travel time: ${error instanceof Error ? error.message : String(error)}`); + console.warn(`Failed to get average travel time: ${error instanceof Error ? error.message : String(error)}`); + return; } } diff --git a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts b/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts index 68f321e..834870c 100644 --- a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts +++ b/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, it, expect, afterEach } from "@jest/globals"; import { RedisShuttleRepository } from "../RedisShuttleRepository"; import { generateMockShuttles } from "../../../../testHelpers/mockDataGenerators"; +import { IOrderedStop } from "../../../entities/ShuttleRepositoryEntities"; describe("RedisShuttleRepository", () => { let repository: RedisShuttleRepository; @@ -44,22 +45,27 @@ describe("RedisShuttleRepository", () => { await repository.addOrUpdateStop(stop1); await repository.addOrUpdateStop(stop2); - const orderedStop1 = { + const orderedStop1: IOrderedStop = { routeId: route.id, stopId: stop1.id, position: 1, systemId: systemId, updatedTime: new Date(), }; - const orderedStop2 = { + const orderedStop2: IOrderedStop = { routeId: route.id, stopId: stop2.id, position: 2, systemId: systemId, updatedTime: new Date(), }; + orderedStop1.nextStop = orderedStop2; + orderedStop1.previousStop = orderedStop2; + orderedStop2.nextStop = orderedStop1; + orderedStop2.previousStop = orderedStop1; await repository.addOrUpdateOrderedStop(orderedStop1); await repository.addOrUpdateOrderedStop(orderedStop2); + return { route, systemId, @@ -90,7 +96,7 @@ describe("RedisShuttleRepository", () => { it("updates how long the shuttle took to get from one stop to another", async () => { const { route, systemId, stop2, stop1 } = await setupRouteAndOrderedStops(); - + // Start the shuttle at stop 1, then have it move to stop 2 const shuttle = { id: "sh1", @@ -120,6 +126,39 @@ describe("RedisShuttleRepository", () => { }); expect(travelTime).toEqual(15 * 60) }); + + it("adds an ETA entry based on historical data", async () => { + const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); + + // Start the shuttle at stop 1, then have it move to stop 2 + const shuttle = { + id: "sh1", + name: "Shuttle 1", + routeId: route.id, + systemId: systemId, + coordinates: stop1.coordinates, + orientationInDegrees: 0, + updatedTime: new Date(), + } + + const firstStopArrivalTime = new Date(2025, 0, 1, 12, 0, 0); + await repository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); + + shuttle.coordinates = stop2.coordinates; + // 15-minute travel time + const secondStopArrivalTime = new Date(2025, 0, 1, 12, 15, 0); + await repository.addOrUpdateShuttle(shuttle, secondStopArrivalTime.getTime()); + + shuttle.coordinates = stop1.coordinates; + await repository.addOrUpdateShuttle( + shuttle, + new Date(2025, 0, 8, 12, 0, 0).getTime(), + new Date(2025, 0, 8, 12, 7, 30), + ); + + const eta = await repository.getEtaForShuttleAndStopId(shuttle.id, stop2.id); + expect(eta?.secondsRemaining).toEqual(7 * 60 + 30); + }); }); describe("getArrivedStopIfExists", () => {