UNPKG

minotor

Version:

A lightweight client-side transit routing library.

1,039 lines (944 loc) 32 kB
import assert from 'node:assert'; import { describe, it } from 'node:test'; import { PickUpDropOffTypes, Route } from '../route.js'; import { timeFromHMS } from '../time.js'; import { RouteType, RouteTypes, ServiceRoute, StopAdjacency, Timetable, TransferTypes, TripStop, TripTransfers, } from '../timetable.js'; import { encode } from '../tripStopId.js'; describe('Timetable', () => { const stopsAdjacency: StopAdjacency[] = [ { routes: [] }, { transfers: [{ destination: 2, type: TransferTypes.RECOMMENDED }], routes: [0, 1], }, { transfers: [ { destination: 1, type: TransferTypes.GUARANTEED, minTransferTime: 3, }, ], routes: [1, 0], }, { routes: [], }, ]; const route1 = Route.of({ id: 0, serviceRouteId: 0, trips: [ { stops: [ { id: 1, arrivalTime: timeFromHMS(16, 40, 0), departureTime: timeFromHMS(16, 50, 0), }, { id: 2, arrivalTime: timeFromHMS(17, 20, 0), departureTime: timeFromHMS(17, 30, 0), pickUpType: PickUpDropOffTypes.NOT_AVAILABLE, }, ], }, { stops: [ { id: 1, arrivalTime: timeFromHMS(18, 0, 0), departureTime: timeFromHMS(18, 10, 0), }, { id: 2, arrivalTime: timeFromHMS(19, 0, 0), departureTime: timeFromHMS(19, 10, 0), }, ], }, ], }); const route2 = Route.of({ id: 1, serviceRouteId: 1, trips: [ { stops: [ { id: 2, arrivalTime: timeFromHMS(18, 20, 0), departureTime: timeFromHMS(18, 30, 0), }, { id: 1, arrivalTime: timeFromHMS(19, 0, 0), departureTime: timeFromHMS(19, 10, 0), }, ], }, ], }); const routesAdjacency = [route1, route2]; const routes: ServiceRoute[] = [ { type: RouteTypes.RAIL, name: 'Route 1', routes: [0] }, { type: RouteTypes.RAIL, name: 'Route 2', routes: [1] }, ]; const sampleTimetable: Timetable = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), new Map(), ); it('should serialize a timetable to a Uint8Array', () => { const serializedData = sampleTimetable.serialize(); assert(serializedData instanceof Uint8Array); assert(serializedData.length > 0); }); it('should deserialize a Uint8Array to a timetable', () => { const serializedData = sampleTimetable.serialize(); const deserializedTimetable = Timetable.fromData(serializedData); assert.deepStrictEqual(deserializedTimetable, sampleTimetable); }); it('should find the earliest trip for stop1 on route1', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const route = sampleTimetable.getRoute(0)!; const tripIndex = route.findEarliestTrip(0); assert.strictEqual(tripIndex, 0); }); it('should find the earliest trip for stop1 on route1 after a specific time', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const route = sampleTimetable.getRoute(0)!; const afterTime = timeFromHMS(17, 0, 0); const tripIndex = route.findEarliestTrip(0, afterTime); assert.strictEqual(tripIndex, 1); }); it('should return undefined if no valid trip exists after a specific time', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const route = sampleTimetable.getRoute(0)!; const afterTime = timeFromHMS(23, 40, 0); const tripIndex = route.findEarliestTrip(0, afterTime); assert.strictEqual(tripIndex, undefined); }); describe('findReachableRoutes', () => { it('should find reachable routes from a single stop', () => { const fromStops = new Set([1]); const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); assert.strictEqual(reachableRoutes.size, 2); assert.deepStrictEqual( reachableRoutes, new Map([ [route1, 0], [route2, 1], ]), ); }); it('should find reachable routes from multiple stops', () => { const fromStops = new Set([1, 2]); const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); assert.strictEqual(reachableRoutes.size, 2); assert.deepStrictEqual( reachableRoutes, new Map([ [route1, 0], [route2, 0], ]), ); }); it('should find no reachable routes from stops with no routes', () => { const fromStops = new Set([3]); // Stop 3 has no routes in sample timetable const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); assert.strictEqual(reachableRoutes.size, 0); assert.deepStrictEqual(reachableRoutes, new Map()); }); it('should find no reachable routes from empty stop set', () => { const fromStops = new Set<number>(); const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); assert.strictEqual(reachableRoutes.size, 0); assert.deepStrictEqual(reachableRoutes, new Map()); }); it('should find no reachable routes from non-existent stops', () => { const fromStops = new Set([999, 1000]); // Non-existent stops const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); assert.strictEqual(reachableRoutes.size, 0); assert.deepStrictEqual(reachableRoutes, new Map()); }); it('should filter routes by transport modes correctly', () => { const fromStops = new Set([1]); const railRoutes = sampleTimetable.findReachableRoutes( fromStops, new Set<RouteType>([RouteTypes.RAIL]), ); assert.strictEqual(railRoutes.size, 2); assert.deepStrictEqual( railRoutes, new Map([ [route1, 0], [route2, 1], ]), ); const busRoutes = sampleTimetable.findReachableRoutes( fromStops, new Set<RouteType>([RouteTypes.BUS]), ); assert.strictEqual(busRoutes.size, 0); assert.deepStrictEqual(busRoutes, new Map()); const multiModeRoutes = sampleTimetable.findReachableRoutes( fromStops, new Set<RouteType>([ RouteTypes.RAIL, RouteTypes.BUS, RouteTypes.SUBWAY, ]), ); assert.strictEqual(multiModeRoutes.size, 2); }); it('should return earliest hop-on stop when route is accessible from multiple stops', () => { // Create scenario where same route is accessible from multiple stops in the query // route1 has stops [1, 2] in that order, so we need to test with those actual stops const fromStops = new Set([1, 2]); // Both stops are on route1 const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops); // route1 should use stop index 0 (stop 1 comes before stop 2 on route1) // route2 should use stop index 0 (stop 2 comes before stop 1 on route2) assert.strictEqual(reachableRoutes.size, 2); assert.deepStrictEqual( reachableRoutes, new Map([ [route1, 0], // Stop index 0 (stop 1) comes before stop index 1 (stop 2) on route1 [route2, 0], // Stop index 0 (stop 2) comes before stop index 1 (stop 1) on route2 ]), ); }); describe('getContinuousTrips', () => { it('should return empty array when stop has no trip continuations', () => { const continuousTrips = sampleTimetable.getContinuousTrips(0, 0, 0); assert.deepStrictEqual(continuousTrips, []); }); it('should return empty array when stop has trip continuations but not for the specified trip', () => { // Create a timetable with trip continuations that don't match the query const tripContinuationsMap = new Map([ [encode(0, 0, 1), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]], // Different trip index ]); const stopsWithContinuations: StopAdjacency[] = [ { routes: [] }, { routes: [0, 1], }, { routes: [1] }, ]; const timetableWithContinuations = new Timetable( stopsWithContinuations, routesAdjacency, routes, tripContinuationsMap, ); const continuousTrips = timetableWithContinuations.getContinuousTrips( 0, 0, 0, ); // Query trip index 0, but continuations are for trip index 1 assert.deepStrictEqual(continuousTrips, []); }); it('should return trip continuations when they exist for the specified trip', () => { const expectedContinuations: TripStop[] = [ { stopIndex: 0, routeId: 1, tripIndex: 0 }, { stopIndex: 0, routeId: 1, tripIndex: 1 }, ]; const tripContinuationsMap = new Map([ [encode(0, 0, 0), expectedContinuations], ]); const stopsWithContinuations: StopAdjacency[] = [ { routes: [] }, { routes: [0, 1], }, { routes: [1] }, ]; const timetableWithContinuations = new Timetable( stopsWithContinuations, routesAdjacency, routes, tripContinuationsMap, ); const continuousTrips = timetableWithContinuations.getContinuousTrips( 0, 0, 0, ); assert.deepStrictEqual(continuousTrips, expectedContinuations); }); it('should return empty array when querying with non-matching parameters', () => { const continuousTrips = sampleTimetable.getContinuousTrips(999, 0, 0); assert.deepStrictEqual(continuousTrips, []); }); }); describe('getGuaranteedTripTransfers', () => { it('should return empty array when stop has no guaranteed trip transfers', () => { const guaranteedTransfers = sampleTimetable.getGuaranteedTripTransfers( 0, 0, 0, ); assert.deepStrictEqual(guaranteedTransfers, []); }); it('should return empty array when stop has guaranteed trip transfers but not for the specified trip', () => { const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 1), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]], // Different trip index ]); const stopsWithGuaranteedTransfers: StopAdjacency[] = [ { routes: [] }, { routes: [0, 1], }, { routes: [1] }, ]; const timetableWithGuaranteedTransfers = new Timetable( stopsWithGuaranteedTransfers, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const guaranteedTransfers = timetableWithGuaranteedTransfers.getGuaranteedTripTransfers(0, 0, 0); // Query trip index 0, but transfers are for trip index 1 assert.deepStrictEqual(guaranteedTransfers, []); }); it('should return guaranteed trip transfers when they exist for the specified trip', () => { const expectedTransfers: TripStop[] = [ { stopIndex: 0, routeId: 1, tripIndex: 0 }, { stopIndex: 0, routeId: 1, tripIndex: 1 }, ]; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), expectedTransfers], ]); const stopsWithGuaranteedTransfers: StopAdjacency[] = [ { routes: [] }, { routes: [0, 1], }, { routes: [1] }, ]; const timetableWithGuaranteedTransfers = new Timetable( stopsWithGuaranteedTransfers, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const guaranteedTransfers = timetableWithGuaranteedTransfers.getGuaranteedTripTransfers(0, 0, 0); assert.deepStrictEqual(guaranteedTransfers, expectedTransfers); }); it('should return empty array when querying with non-matching parameters', () => { const guaranteedTransfers = sampleTimetable.getGuaranteedTripTransfers( 999, 0, 0, ); assert.deepStrictEqual(guaranteedTransfers, []); }); }); describe('isTripTransferGuaranteed', () => { it('should return false when no guaranteed transfers exist', () => { const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; const toTripStop: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0 }; const result = sampleTimetable.isTripTransferGuaranteed( fromTripStop, toTripStop, ); assert.strictEqual(result, false); }); it('should return false when guaranteed transfers exist but not for the specified trip', () => { const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 1), [{ stopIndex: 0, routeId: 1, tripIndex: 0 }]], // Transfers for trip index 1 ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; // Query trip index 0 const toTripStop: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0 }; const result = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, toTripStop, ); assert.strictEqual(result, false); }); it('should return true when the transfer is guaranteed', () => { const guaranteedTransfer: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0, }; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), [guaranteedTransfer]], ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; const toTripStop: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0 }; const result = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, toTripStop, ); assert.strictEqual(result, true); }); it('should return false when transfer destination does not match any guaranteed transfer', () => { const guaranteedTransfer: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0, }; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), [guaranteedTransfer]], ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; const nonMatchingToTripStop: TripStop = { stopIndex: 1, routeId: 1, tripIndex: 0, }; // Different stopIndex const result = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, nonMatchingToTripStop, ); assert.strictEqual(result, false); }); it('should return true when transfer matches one of multiple guaranteed transfers', () => { const guaranteedTransfers: TripStop[] = [ { stopIndex: 0, routeId: 1, tripIndex: 0 }, { stopIndex: 0, routeId: 1, tripIndex: 1 }, { stopIndex: 1, routeId: 1, tripIndex: 0 }, ]; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), guaranteedTransfers], ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; // Test matching the second guaranteed transfer const toTripStop: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 1 }; const result = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, toTripStop, ); assert.strictEqual(result, true); // Test matching the third guaranteed transfer const toTripStop2: TripStop = { stopIndex: 1, routeId: 1, tripIndex: 0, }; const result2 = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, toTripStop2, ); assert.strictEqual(result2, true); }); it('should return false for invalid trip data (non-existent route)', () => { const guaranteedTransfer: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0, }; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), [guaranteedTransfer]], ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 999, tripIndex: 0, }; // Non-existent route const toTripStop: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0 }; const result = timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, toTripStop, ); assert.strictEqual(result, false); }); it('should correctly distinguish between different stopIndex values', () => { const guaranteedTransfer: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0, }; const guaranteedTripTransfersMap = new Map([ [encode(0, 0, 0), [guaranteedTransfer]], ]); const timetableWithGuaranteedTransfers = new Timetable( stopsAdjacency, routesAdjacency, routes, new Map(), guaranteedTripTransfersMap, ); const fromTripStop: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; // Exact match should return true const exactMatch: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 0 }; assert.strictEqual( timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, exactMatch, ), true, ); // Different stopIndex should return false const differentStopIndex: TripStop = { stopIndex: 1, routeId: 1, tripIndex: 0, }; assert.strictEqual( timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, differentStopIndex, ), false, ); // Different routeId should return false const differentRouteId: TripStop = { stopIndex: 0, routeId: 0, tripIndex: 0, }; assert.strictEqual( timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, differentRouteId, ), false, ); // Different tripIndex should return false const differentTripIndex: TripStop = { stopIndex: 0, routeId: 1, tripIndex: 1, }; assert.strictEqual( timetableWithGuaranteedTransfers.isTripTransferGuaranteed( fromTripStop, differentTripIndex, ), false, ); }); }); describe('findFirstBoardableTrip', () => { const BOARDING_STOP_INDEX = 0; const FFBT_ROUTE_ID = 2; const ffbtRoute = Route.of({ id: FFBT_ROUTE_ID, serviceRouteId: 2, trips: [ { stops: [ { id: 0, arrivalTime: timeFromHMS(8, 0, 0), departureTime: timeFromHMS(8, 0, 0), }, { id: 1, arrivalTime: timeFromHMS(8, 30, 0), departureTime: timeFromHMS(8, 30, 0), }, ], }, { stops: [ { id: 0, arrivalTime: timeFromHMS(9, 0, 0), departureTime: timeFromHMS(9, 0, 0), pickUpType: PickUpDropOffTypes.NOT_AVAILABLE, }, { id: 1, arrivalTime: timeFromHMS(9, 30, 0), departureTime: timeFromHMS(9, 30, 0), }, ], }, { stops: [ { id: 0, arrivalTime: timeFromHMS(10, 0, 0), departureTime: timeFromHMS(10, 0, 0), }, { id: 1, arrivalTime: timeFromHMS(10, 30, 0), departureTime: timeFromHMS(10, 30, 0), }, ], }, { stops: [ { id: 0, arrivalTime: timeFromHMS(11, 0, 0), departureTime: timeFromHMS(11, 0, 0), }, { id: 1, arrivalTime: timeFromHMS(11, 30, 0), departureTime: timeFromHMS(11, 30, 0), }, ], }, ], }); const ffbtStopsAdjacency: StopAdjacency[] = [ { routes: [FFBT_ROUTE_ID] }, { routes: [FFBT_ROUTE_ID] }, ]; const ffbtServiceRoutes = [ { type: RouteTypes.BUS, name: 'Route 2', routes: [FFBT_ROUTE_ID] }, ]; const ffbtTimetable = new Timetable( ffbtStopsAdjacency, [ffbtRoute], ffbtServiceRoutes, ); describe('without fromTripStop', () => { it('returns the first trip with an available pickup', () => { const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, ); assert.strictEqual(result, 0); }); it('skips trips with NOT_AVAILABLE pickup', () => { // Start scanning from trip 1 (NOT_AVAILABLE) → must return trip 2. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 1, ); assert.strictEqual(result, 2); }); it('starts scanning at earliestTrip', () => { // Even though trip 0 is valid, scanning starts at trip 2. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 2, ); assert.strictEqual(result, 2); }); it('respects the beforeTrip exclusive upper bound', () => { // beforeTrip=1 means only trip 0 is in scope. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, 0, 1, ); assert.strictEqual(result, 0); }); it('returns undefined when the only trip in range has a NOT_AVAILABLE pickup', () => { // [1, 2) contains only trip 1, which is NOT_AVAILABLE. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 1, 0, 2, ); assert.strictEqual(result, undefined); }); it('returns undefined when earliestTrip is past all trips', () => { const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 99, ); assert.strictEqual(result, undefined); }); it('returns undefined when beforeTrip equals earliestTrip (empty range)', () => { const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 2, 0, 2, ); assert.strictEqual(result, undefined); }); }); describe('with fromTripStop', () => { const fromTripStop: TripStop = { routeId: 99, stopIndex: 0, tripIndex: 0, }; it('returns the first trip whose departure satisfies after + transferTime', () => { // after=09:30, transferTime=30 → requiredTime=10:00. // trip 0 (08:00) < 10:00 → skip. // trip 1 (09:00) NOT_AVAILABLE → skip. // trip 2 (10:00) ≥ 10:00 → returned. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(9, 30, 0), undefined, fromTripStop, 30, ); assert.strictEqual(result, 2); }); it('treats departure exactly equal to after as satisfying (transferTime=0)', () => { // after=10:00, transferTime=0 → requiredTime=10:00. // trip 0 (08:00) < 10:00 → skip. // trip 1 (09:00) NOT_AVAILABLE → skip. // trip 2 (10:00) ≥ 10:00 → returned. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(10, 0, 0), undefined, fromTripStop, 0, ); assert.strictEqual(result, 2); }); it('returns undefined when all trips depart before after + transferTime', () => { // after=11:00, transferTime=30 → requiredTime=11:30. // trip 3 (11:00) < 11:30 → skip. No further trips. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(11, 0, 0), undefined, fromTripStop, 30, ); assert.strictEqual(result, undefined); }); it('returns undefined when beforeTrip excludes all trips that would satisfy the requirement', () => { // Only trips 0 and 1 are in scope (beforeTrip=2). // after=09:00, transferTime=30 → requiredTime=09:30. // trip 0 (08:00) < 09:30 → skip. // trip 1 (09:00) NOT_AVAILABLE → skip. // beforeTrip reached → undefined. const result = ffbtTimetable.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(9, 0, 0), 2, fromTripStop, 30, ); assert.strictEqual(result, undefined); }); }); describe('guaranteed connection', () => { it('bypasses the after + transferTime departure check', () => { const fromTripStop: TripStop = { routeId: 99, stopIndex: 0, tripIndex: 0, }; const guaranteedTransfers: TripTransfers = new Map([ [ encode( fromTripStop.stopIndex, fromTripStop.routeId, fromTripStop.tripIndex, ), [ { stopIndex: BOARDING_STOP_INDEX, routeId: FFBT_ROUTE_ID, tripIndex: 0, }, ], ], ]); const timetableWithGuarantee = new Timetable( ffbtStopsAdjacency, [ffbtRoute], ffbtServiceRoutes, new Map(), guaranteedTransfers, ); // after=07:00, transferTime=120 → requiredTime=09:00; trip 0 (08:00) is below // that threshold but the guarantee returns it immediately. const result = timetableWithGuarantee.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(7, 0, 0), undefined, fromTripStop, 120, ); assert.strictEqual(result, 0); }); it('does not bypass a NOT_AVAILABLE pickup', () => { const fromTripStop: TripStop = { routeId: 99, stopIndex: 0, tripIndex: 0, }; const guaranteedTransfers: TripTransfers = new Map([ [ encode( fromTripStop.stopIndex, fromTripStop.routeId, fromTripStop.tripIndex, ), [ { stopIndex: BOARDING_STOP_INDEX, routeId: FFBT_ROUTE_ID, tripIndex: 1, }, ], ], ]); const timetableWithGuarantee = new Timetable( ffbtStopsAdjacency, [ffbtRoute], ffbtServiceRoutes, new Map(), guaranteedTransfers, ); // after=08:30, transferTime=60 → requiredTime=09:30. // trip 0 (08:00): not guaranteed for trip 0, 08:00 < 09:30 → skip. // trip 1 (09:00): NOT_AVAILABLE → skip (guarantee is irrelevant). // trip 2 (10:00): no guarantee, but 10:00 ≥ 09:30 → first boardable. const result = timetableWithGuarantee.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(8, 30, 0), undefined, fromTripStop, 60, ); assert.strictEqual(result, 2); }); it('applies only to its declared target trip index', () => { const fromTripStop: TripStop = { routeId: 99, stopIndex: 0, tripIndex: 0, }; const guaranteedTransfers: TripTransfers = new Map([ [ encode( fromTripStop.stopIndex, fromTripStop.routeId, fromTripStop.tripIndex, ), [ { stopIndex: BOARDING_STOP_INDEX, routeId: FFBT_ROUTE_ID, tripIndex: 2, }, ], ], ]); const timetableWithGuarantee = new Timetable( ffbtStopsAdjacency, [ffbtRoute], ffbtServiceRoutes, new Map(), guaranteedTransfers, ); // after=09:30, transferTime=120 → requiredTime=11:30. // trip 0 (08:00): not guaranteed, 08:00 < 11:30 → skip. // trip 1 (09:00): NOT_AVAILABLE → skip. // trip 2 (10:00): guaranteed → returned immediately despite 10:00 < 11:30. const result = timetableWithGuarantee.findFirstBoardableTrip( BOARDING_STOP_INDEX, ffbtRoute, 0, timeFromHMS(9, 30, 0), undefined, fromTripStop, 120, ); assert.strictEqual(result, 2); }); }); }); }); });