From 53bdaa31378bf38673d518a77aee24798196d9fc Mon Sep 17 00:00:00 2001 From: Ridwan Aditama Date: Fri, 13 Nov 2026 10:02:33 +0100 Subject: [PATCH 1/2] fix: treat negative maximumWaitTime as no wait limit --- .../__tests__/transfer-risk.test.ts | 19 +++++++++++++++++++ src/transfer-risk/transfer-risk.ts | 9 ++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/transfer-risk/__tests__/transfer-risk.test.ts b/src/transfer-risk/__tests__/transfer-risk.test.ts index 68b5eb8..39d1324 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({ diff --git a/src/transfer-risk/transfer-risk.ts b/src/transfer-risk/transfer-risk.ts index 8c3d22d..acf9656 100644 --- a/src/transfer-risk/transfer-risk.ts +++ b/src/transfer-risk/transfer-risk.ts @@ -102,8 +102,9 @@ 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. Unparseable times keep + * the guarantee, so bad data suppresses a warning rather than inventing one. */ const transferHolds = ( alightedFrom: TransferLeg, @@ -112,7 +113,9 @@ const transferHolds = ( ): 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; From ad84705d9a4a00fd1fa538b5b95f518188e93e95 Mon Sep 17 00:00:00 2001 From: Ridwan Aditama Date: Fri, 13 Nov 2026 11:26:14 +0100 Subject: [PATCH 2/2] fix: consider walk time on guaranteed interchanges --- .../__tests__/transfer-risk.test.ts | 42 ++++++++++++++++++- src/transfer-risk/transfer-risk.ts | 13 +++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/transfer-risk/__tests__/transfer-risk.test.ts b/src/transfer-risk/__tests__/transfer-risk.test.ts index 39d1324..5e1968e 100644 --- a/src/transfer-risk/__tests__/transfer-risk.test.ts +++ b/src/transfer-risk/__tests__/transfer-risk.test.ts @@ -234,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', @@ -249,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 acf9656..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), @@ -103,13 +103,16 @@ 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. Entur sends `-1` rather than null when no - * limit is set, so a negative value counts as absent. Unparseable times keep - * the guarantee, so bad data suppresses a warning rather than inventing one. + * 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; @@ -119,7 +122,7 @@ const transferHolds = ( 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;