diff --git a/src/transfer-risk/__tests__/transfer-risk.test.ts b/src/transfer-risk/__tests__/transfer-risk.test.ts index 68b5eb8..5e1968e 100644 --- a/src/transfer-risk/__tests__/transfer-risk.test.ts +++ b/src/transfer-risk/__tests__/transfer-risk.test.ts @@ -145,6 +145,25 @@ describe('getLegTransferRisk', () => { expect(getLegTransferRisk(legs, 1)).toBeUndefined(); }); + it('treats a negative maximum wait time as no stated wait limit', () => { + const legs = [ + transitLeg({ + expectedEndTime: '2024-01-01T10:10:00.000Z', + interchangeTo: {guaranteed: true, maximumWaitTime: -1}, + }), + footLeg({ + expectedStartTime: '2024-01-01T10:10:00.000Z', + expectedEndTime: '2024-01-01T10:10:12.000Z', + }), + transitLeg({ + aimedStartTime: '2024-01-01T10:10:00.000Z', + expectedStartTime: '2024-01-01T10:10:00.000Z', + }), + ]; + // Entur's "not set" sentinel, not a deadline one second before departure. + expect(getLegTransferRisk(legs, 2)).toBeUndefined(); + }); + it('warns when the interchange is explicitly not guaranteed', () => { const legs = [ transitLeg({ @@ -215,7 +234,7 @@ describe('getLegTransferRisk', () => { expect(getLegTransferRisk(legs, 1)).toBe('uncertain'); }); - it('counts an intervening walk against the maximum wait time', () => { + it('does not count an intervening walk against the maximum wait time', () => { const legs = [ transitLeg({ expectedEndTime: '2024-01-01T10:10:00.000Z', @@ -230,7 +249,45 @@ describe('getLegTransferRisk', () => { expectedStartTime: '2024-01-01T10:11:00.000Z', }), ]; - // Held until 10:13, but the walk does not end until 10:15. + // Held until 10:13 and the first service arrives at 10:10. The walk + // ending at 10:15 is covered by the guarantee. + expect(getLegTransferRisk(legs, 2)).toBeUndefined(); + }); + + it('passes a zero maximum wait time when only the walk ends late', () => { + const legs = [ + transitLeg({ + expectedEndTime: '2024-01-01T10:10:00.000Z', + interchangeTo: {guaranteed: true, maximumWaitTime: 0}, + }), + footLeg({ + expectedStartTime: '2024-01-01T10:10:00.000Z', + expectedEndTime: '2024-01-01T10:10:12.000Z', + }), + transitLeg({ + aimedStartTime: '2024-01-01T10:10:00.000Z', + expectedStartTime: '2024-01-01T10:10:00.000Z', + }), + ]; + expect(getLegTransferRisk(legs, 2)).toBeUndefined(); + }); + + it('warns through a walk once the first service is past the maximum wait time', () => { + const legs = [ + transitLeg({ + expectedEndTime: '2024-01-01T10:14:00.000Z', + interchangeTo: {guaranteed: true, maximumWaitTime: 120}, + }), + footLeg({ + expectedStartTime: '2024-01-01T10:14:00.000Z', + expectedEndTime: '2024-01-01T10:15:00.000Z', + }), + transitLeg({ + aimedStartTime: '2024-01-01T10:11:00.000Z', + expectedStartTime: '2024-01-01T10:11:00.000Z', + }), + ]; + // Held until 10:13, but the first service does not arrive until 10:14. expect(getLegTransferRisk(legs, 2)).toBe('uncertain'); }); diff --git a/src/transfer-risk/transfer-risk.ts b/src/transfer-risk/transfer-risk.ts index 8c3d22d..4d995c8 100644 --- a/src/transfer-risk/transfer-risk.ts +++ b/src/transfer-risk/transfer-risk.ts @@ -37,7 +37,7 @@ export const getLegTransferRisk = ( const alightedFrom = previousTransitLeg(legs, index); if (!alightedFrom) return undefined; - if (transferHolds(alightedFrom, boarding, arriveAt)) return undefined; + if (transferHolds(alightedFrom, boarding)) return undefined; return getTransferRisk( secondsBetween(arriveAt.expectedEndTime, boarding.expectedStartTime), @@ -102,21 +102,27 @@ const previousTransitLeg = ( /** * Whether the interchange still guarantees the transfer. A guarantee lasts * `maximumWaitTime` seconds past the connecting service's scheduled departure; - * absent, it waits indefinitely. Unparseable times keep the guarantee, so bad - * data suppresses a warning rather than inventing one. + * absent, it waits indefinitely. Entur sends `-1` rather than null when no + * limit is set, so a negative value counts as absent. + * + * Arrival is when the service you alight from arrives, not the end of a walk + * after it: Entur counts walk time and slack as covered by the guarantee. + * Unparseable times keep the guarantee, so bad data suppresses a warning rather + * than inventing one. */ const transferHolds = ( alightedFrom: TransferLeg, boarding: TransferLeg, - arriveAt: TransferLeg, ): boolean => { const interchange = alightedFrom.interchangeTo; if (interchange?.guaranteed !== true) return false; - if (interchange.maximumWaitTime == null) return true; + if (interchange.maximumWaitTime == null || interchange.maximumWaitTime < 0) { + return true; + } const deadline = toEpochMs(boarding.aimedStartTime) + interchange.maximumWaitTime * 1000; - const arrival = toEpochMs(arriveAt.expectedEndTime); + const arrival = toEpochMs(alightedFrom.expectedEndTime); if (!Number.isFinite(deadline) || !Number.isFinite(arrival)) return true; return arrival <= deadline;