Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions src/transfer-risk/__tests__/transfer-risk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand All @@ -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');
});

Expand Down
18 changes: 12 additions & 6 deletions src/transfer-risk/transfer-risk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand Down
Loading