minotor
Version:
A lightweight client-side transit routing library.
335 lines (300 loc) • 10.2 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import assert from 'node:assert';
import { describe, it } from 'node:test';
import { timeFromHM } from '../../timetable/time.js';
import { RangeRaptorState } from '../rangeState.js';
import { RoutingState, UNREACHED_TIME } from '../state.js';
const NB_STOPS = 4;
const MAX_ROUNDS = 3;
describe('RangeRaptorState', () => {
describe('constructor', () => {
it('creates roundLabels of length maxRounds + 2', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
assert.strictEqual(state.roundLabels.length, MAX_ROUNDS + 2);
});
it('initialized all roundLabels to UNREACHED_TIME', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
for (const round of state.roundLabels) {
for (const val of round) {
assert.strictEqual(val, UNREACHED_TIME);
}
}
});
it('stores latestDeparture', () => {
const latest = timeFromHM(11, 30);
const state = new RangeRaptorState(MAX_ROUNDS, NB_STOPS, latest);
assert.strictEqual(state.latestDeparture, latest);
});
});
describe('setCurrentRun', () => {
it('seeds round-0 shared labels from origin nodes', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({
nbStops: NB_STOPS,
origins: [1],
graph: [[[1, { stopId: 1, arrival: timeFromHM(9, 0) }]]],
}),
);
assert.strictEqual(state.roundLabels[0]![1], timeFromHM(9, 0));
});
it('skips an origin that has no edge in round 0', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({
nbStops: NB_STOPS,
origins: [1],
graph: [[]],
}),
);
assert.strictEqual(state.roundLabels[0]![1], UNREACHED_TIME);
});
it('delegates origins and graph getters to the active run', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
const run = RoutingState.fromTestData({
nbStops: NB_STOPS,
origins: [0, 2],
graph: [[]],
});
state.setCurrentRun(run);
assert.deepStrictEqual(state.origins, [0, 2]);
assert.strictEqual(state.graph, run.graph);
});
});
describe('improvementBound', () => {
it('returns the cross-run shared label, tighter than the per-run arrival', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
state.updateArrival(2, timeFromHM(9, 30), 1);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
assert.strictEqual(state.arrivalTime(2), UNREACHED_TIME);
assert.strictEqual(state.improvementBound(1, 2), timeFromHM(9, 30));
});
});
describe('updateArrival', () => {
it('improves the shared roundLabel for the given round and stop', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
state.updateArrival(2, timeFromHM(9, 0), 1);
assert.strictEqual(state.roundLabels[1]![2], timeFromHM(9, 0));
});
it('does not worsen a shared roundLabel that is already tight', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
state.updateArrival(2, timeFromHM(9, 0), 1);
state.updateArrival(2, timeFromHM(9, 30), 1);
assert.strictEqual(state.roundLabels[1]![2], timeFromHM(9, 0));
});
it('updates destinationBest when a destination stop is first reached', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [3] }),
);
assert.strictEqual(state.destinationBest, UNREACHED_TIME);
state.updateArrival(3, timeFromHM(10, 0), 1);
assert.strictEqual(state.destinationBest, timeFromHM(10, 0));
});
it('destinationBest persists when the run is swapped', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [3] }),
);
state.updateArrival(3, timeFromHM(10, 0), 1);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [3] }),
);
assert.strictEqual(state.destinationBest, timeFromHM(10, 0));
});
});
describe('initRound', () => {
it('propagates the round k-1 label into round k for changed stops', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
state.updateArrival(2, timeFromHM(9, 0), 1);
state.initRound(2);
assert.strictEqual(state.roundLabels[2]![2], timeFromHM(9, 0));
});
it('does not overwrite a tighter label already present in round k', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [2] }),
);
state.updateArrival(2, timeFromHM(8, 30), 2);
state.updateArrival(2, timeFromHM(9, 0), 1);
state.initRound(2);
assert.strictEqual(state.roundLabels[2]![2], timeFromHM(8, 30));
});
it('is a no-op when no stop changed in the previous round', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(RoutingState.fromTestData({ nbStops: NB_STOPS }));
state.initRound(1);
for (const val of state.roundLabels[1]!) {
assert.strictEqual(val, UNREACHED_TIME);
}
});
it('clears the changed-stop list so a subsequent call propagates nothing new', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(RoutingState.fromTestData({ nbStops: NB_STOPS }));
state.updateArrival(2, timeFromHM(9, 0), 0);
state.initRound(1);
state.initRound(2);
assert.strictEqual(state.roundLabels[2]![2], UNREACHED_TIME);
});
});
describe('updateArrival aggregate overwrite behavior', () => {
it('does not overwrite the current run aggregate with a later arrival from a higher round', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
const run = RoutingState.fromTestData({
nbStops: NB_STOPS,
destinations: [2],
arrivals: [[2, timeFromHM(8, 30), 1]],
});
state.setCurrentRun(run);
state.updateArrival(2, timeFromHM(8, 45), 2);
assert.deepStrictEqual(run.getArrival(2), {
arrival: timeFromHM(8, 30),
legNumber: 1,
});
assert.strictEqual(state.roundLabels[2]![2], timeFromHM(8, 45));
});
it('does not overwrite the current run aggregate with an equal-time arrival using more legs', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
const run = RoutingState.fromTestData({
nbStops: NB_STOPS,
destinations: [2],
arrivals: [[2, timeFromHM(8, 30), 2]],
});
state.setCurrentRun(run);
state.updateArrival(2, timeFromHM(8, 30), 3);
assert.deepStrictEqual(run.getArrival(2), {
arrival: timeFromHM(8, 30),
legNumber: 2,
});
assert.strictEqual(state.roundLabels[3]![2], timeFromHM(8, 30));
});
it('prefers fewer legs for the current run aggregate when arrival time is equal', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
const run = RoutingState.fromTestData({
nbStops: NB_STOPS,
destinations: [2],
arrivals: [[2, timeFromHM(8, 30), 3]],
});
state.setCurrentRun(run);
state.updateArrival(2, timeFromHM(8, 30), 2);
assert.deepStrictEqual(run.getArrival(2), {
arrival: timeFromHM(8, 30),
legNumber: 2,
});
});
it('still updates the current run aggregate when the new arrival is earlier', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
const run = RoutingState.fromTestData({
nbStops: NB_STOPS,
destinations: [2],
arrivals: [[2, timeFromHM(8, 45), 1]],
});
state.setCurrentRun(run);
state.updateArrival(2, timeFromHM(8, 30), 3);
assert.deepStrictEqual(run.getArrival(2), {
arrival: timeFromHM(8, 30),
legNumber: 3,
});
});
});
describe('isDestination', () => {
it('delegates to the current run', () => {
const state = new RangeRaptorState(
MAX_ROUNDS,
NB_STOPS,
timeFromHM(12, 0),
);
state.setCurrentRun(
RoutingState.fromTestData({ nbStops: NB_STOPS, destinations: [3] }),
);
assert.strictEqual(state.isDestination(3), true);
assert.strictEqual(state.isDestination(0), false);
});
});
});