minotor 11.1.2 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -3,44 +3,144 @@ import { describe, it } from 'node:test';
3
3
 
4
4
  import fs from 'fs';
5
5
 
6
- import { Query, Router, StopsIndex, Timetable } from '../router.js';
7
- import { timeFromString } from '../timetable/time.js';
6
+ import {
7
+ Query,
8
+ RangeQuery,
9
+ Route,
10
+ Router,
11
+ Stop,
12
+ StopsIndex,
13
+ Timetable,
14
+ } from '../router.js';
15
+ import { timeFromString, timeToString } from '../timetable/time.js';
8
16
 
9
- const routes = [
17
+ type StopRef = { name: string; platform?: string };
18
+
19
+ type ExpectedVehicleLeg = {
20
+ from: StopRef;
21
+ to: StopRef;
22
+ departure: string;
23
+ arrival: string;
24
+ route: { type: string; name: string };
25
+ };
26
+
27
+ type ExpectedTransferLeg = {
28
+ from: StopRef;
29
+ to: StopRef;
30
+ type: string;
31
+ minTransferTime?: number;
32
+ };
33
+
34
+ type ExpectedAccessLeg = {
35
+ from: StopRef;
36
+ to: StopRef;
37
+ duration: number;
38
+ };
39
+
40
+ type ExpectedLeg = ExpectedVehicleLeg | ExpectedTransferLeg | ExpectedAccessLeg;
41
+
42
+ const stopsPath = new URL('./timetable/stops.bin', import.meta.url).pathname;
43
+ const timetablePath = new URL('./timetable/timetable.bin', import.meta.url)
44
+ .pathname;
45
+
46
+ const stopsIndex = StopsIndex.fromData(fs.readFileSync(stopsPath));
47
+ const timetable = Timetable.fromData(fs.readFileSync(timetablePath));
48
+ const router = new Router(timetable, stopsIndex);
49
+
50
+ /**
51
+ * Finds a station by its exact name. Asserts that exactly one STATION-type
52
+ * stop with that name exists so a typo in a test causes an immediate failure.
53
+ */
54
+ function findStation(name: string): Stop {
55
+ const match = stopsIndex
56
+ .findStopsByName(name, 20)
57
+ .find((s) => s.name === name && s.locationType === 'STATION');
58
+ assert.ok(match, `Station not found: "${name}"`);
59
+ return match;
60
+ }
61
+
62
+ /**
63
+ * Converts an internal Stop to a StopRef used in expected leg data.
64
+ * Platform is omitted when the stop has none, keeping assertions concise.
65
+ */
66
+ function toStopRef(stop: Stop): StopRef {
67
+ return stop.platform
68
+ ? { name: stop.name, platform: stop.platform }
69
+ : { name: stop.name };
70
+ }
71
+
72
+ /**
73
+ * Converts a Route's legs to the name-based format used in assertions.
74
+ */
75
+ function toExpectedLegs(route: Route): ExpectedLeg[] {
76
+ return route.legs.map((leg) => {
77
+ if ('route' in leg) {
78
+ return {
79
+ from: toStopRef(leg.from),
80
+ to: toStopRef(leg.to),
81
+ departure: timeToString(leg.departureTime),
82
+ arrival: timeToString(leg.arrivalTime),
83
+ route: leg.route,
84
+ };
85
+ } else if ('type' in leg) {
86
+ return {
87
+ from: toStopRef(leg.from),
88
+ to: toStopRef(leg.to),
89
+ type: leg.type,
90
+ ...(leg.minTransferTime !== undefined && {
91
+ minTransferTime: leg.minTransferTime,
92
+ }),
93
+ };
94
+ } else {
95
+ return {
96
+ from: toStopRef(leg.from),
97
+ to: toStopRef(leg.to),
98
+ duration: leg.duration,
99
+ };
100
+ }
101
+ });
102
+ }
103
+
104
+ const pointRoutes: {
105
+ from: string;
106
+ to: string;
107
+ at: string;
108
+ legs: ExpectedLeg[];
109
+ }[] = [
10
110
  {
11
- from: 'Parent8504100',
12
- to: 'Parent8504880',
111
+ from: 'Fribourg/Freiburg',
112
+ to: 'Moléson-sur-Gruyères',
13
113
  at: '08:30',
14
- route: [
114
+ legs: [
15
115
  {
16
- from: '8504100:0:2',
17
- to: '8504086:0:2',
116
+ from: { name: 'Fribourg/Freiburg', platform: '2' },
117
+ to: { name: 'Bulle', platform: '2' },
18
118
  departure: '08:34',
19
119
  arrival: '09:10',
20
120
  route: { type: 'RAIL', name: 'RE2' },
21
121
  },
22
122
  {
23
- from: '8504086:0:2',
24
- to: '8504086:0:4',
123
+ from: { name: 'Bulle', platform: '2' },
124
+ to: { name: 'Bulle', platform: '4' },
25
125
  type: 'REQUIRES_MINIMAL_TIME',
26
126
  minTransferTime: 3,
27
127
  },
28
128
  {
29
- from: '8504086:0:4',
30
- to: '8504077:0:1',
129
+ from: { name: 'Bulle', platform: '4' },
130
+ to: { name: 'Gruyères', platform: '1' },
31
131
  departure: '09:20',
32
132
  arrival: '09:28',
33
133
  route: { type: 'RAIL', name: 'S51' },
34
134
  },
35
135
  {
36
- from: '8504077:0:1',
37
- to: '8577737:0:B',
136
+ from: { name: 'Gruyères', platform: '1' },
137
+ to: { name: 'Gruyères, gare', platform: 'B' },
38
138
  type: 'REQUIRES_MINIMAL_TIME',
39
139
  minTransferTime: 2,
40
140
  },
41
141
  {
42
- from: '8577737:0:B',
43
- to: '8504880:0:10000',
142
+ from: { name: 'Gruyères, gare', platform: 'B' },
143
+ to: { name: 'Moléson-sur-Gruyères' },
44
144
  departure: '09:33',
45
145
  arrival: '09:44',
46
146
  route: { type: 'BUS', name: '263' },
@@ -48,39 +148,39 @@ const routes = [
48
148
  ],
49
149
  },
50
150
  {
51
- from: 'Parent8507000',
52
- to: 'Parent8509253',
151
+ from: 'Bern',
152
+ to: 'St. Moritz',
53
153
  at: '12:30',
54
- route: [
154
+ legs: [
55
155
  {
56
- from: '8507000:0:8',
57
- to: '8503000:0:33',
156
+ from: { name: 'Bern', platform: '8' },
157
+ to: { name: 'Zürich HB', platform: '33' },
58
158
  departure: '12:31',
59
159
  arrival: '13:28',
60
160
  route: { type: 'RAIL', name: 'IC1' },
61
161
  },
62
162
  {
63
- from: '8503000:0:33',
64
- to: '8503000:0:6',
163
+ from: { name: 'Zürich HB', platform: '33' },
164
+ to: { name: 'Zürich HB', platform: '6' },
65
165
  type: 'REQUIRES_MINIMAL_TIME',
66
166
  minTransferTime: 7,
67
167
  },
68
168
  {
69
- from: '8503000:0:6',
70
- to: '8509000:0:9',
169
+ from: { name: 'Zürich HB', platform: '6' },
170
+ to: { name: 'Chur', platform: '9' },
71
171
  departure: '13:38',
72
172
  arrival: '14:52',
73
173
  route: { type: 'RAIL', name: 'IC3' },
74
174
  },
75
175
  {
76
- from: '8509000:0:9',
77
- to: '8509000:0:10',
176
+ from: { name: 'Chur', platform: '9' },
177
+ to: { name: 'Chur', platform: '10' },
78
178
  type: 'REQUIRES_MINIMAL_TIME',
79
179
  minTransferTime: 3,
80
180
  },
81
181
  {
82
- from: '8509000:0:10',
83
- to: '8509253:0:1',
182
+ from: { name: 'Chur', platform: '10' },
183
+ to: { name: 'St. Moritz', platform: '1' },
84
184
  departure: '14:58',
85
185
  arrival: '16:55',
86
186
  route: { type: 'RAIL', name: 'IR38' },
@@ -88,20 +188,20 @@ const routes = [
88
188
  ],
89
189
  },
90
190
  {
91
- from: 'Parent8500010',
92
- to: 'Parent8721202',
191
+ from: 'Basel SBB',
192
+ to: 'Strasbourg',
93
193
  at: '16:50',
94
- route: [
194
+ legs: [
95
195
  {
96
- from: '8500010:0:33',
97
- to: '8718213',
196
+ from: { name: 'Basel SBB', platform: '33' },
197
+ to: { name: 'Saint-Louis (Haut-Rhin)' },
98
198
  departure: '17:08',
99
199
  arrival: '17:16',
100
200
  route: { type: 'RAIL', name: 'TER' },
101
201
  },
102
202
  {
103
- from: '8718213',
104
- to: '8721202',
203
+ from: { name: 'Saint-Louis (Haut-Rhin)' },
204
+ to: { name: 'Strasbourg' },
105
205
  departure: '17:30',
106
206
  arrival: '18:39',
107
207
  route: { type: 'RAIL', name: 'K200' },
@@ -109,52 +209,52 @@ const routes = [
109
209
  ],
110
210
  },
111
211
  {
112
- from: 'Parent8504100',
113
- to: 'Parent8509073',
212
+ from: 'Fribourg/Freiburg',
213
+ to: 'Davos Platz',
114
214
  at: '08:30',
115
- route: [
215
+ legs: [
116
216
  {
117
- from: '8504100:0:3',
118
- to: '8507000:0:10',
217
+ from: { name: 'Fribourg/Freiburg', platform: '3' },
218
+ to: { name: 'Bern', platform: '10' },
119
219
  departure: '08:33',
120
220
  arrival: '08:56',
121
221
  route: { type: 'RAIL', name: 'IR15' },
122
222
  },
123
223
  {
124
- from: '8507000:0:10',
125
- to: '8507000:0:2',
224
+ from: { name: 'Bern', platform: '10' },
225
+ to: { name: 'Bern', platform: '2' },
126
226
  type: 'REQUIRES_MINIMAL_TIME',
127
227
  minTransferTime: 6,
128
228
  },
129
229
  {
130
- from: '8507000:0:2',
131
- to: '8503000:0:34',
230
+ from: { name: 'Bern', platform: '2' },
231
+ to: { name: 'Zürich HB', platform: '34' },
132
232
  departure: '09:02',
133
233
  arrival: '09:58',
134
234
  route: { type: 'RAIL', name: 'IC81' },
135
235
  },
136
236
  {
137
- from: '8503000:0:34',
138
- to: '8503000:0:10',
237
+ from: { name: 'Zürich HB', platform: '34' },
238
+ to: { name: 'Zürich HB', platform: '10' },
139
239
  type: 'REQUIRES_MINIMAL_TIME',
140
240
  minTransferTime: 7,
141
241
  },
142
242
  {
143
- from: '8503000:0:10',
144
- to: '8509002:0:2',
243
+ from: { name: 'Zürich HB', platform: '10' },
244
+ to: { name: 'Landquart', platform: '2' },
145
245
  departure: '10:07',
146
246
  arrival: '11:11',
147
247
  route: { type: 'RAIL', name: 'IC3' },
148
248
  },
149
249
  {
150
- from: '8509002:0:2',
151
- to: '8509002:0:6',
250
+ from: { name: 'Landquart', platform: '2' },
251
+ to: { name: 'Landquart', platform: '6' },
152
252
  type: 'REQUIRES_MINIMAL_TIME',
153
253
  minTransferTime: 4,
154
254
  },
155
255
  {
156
- from: '8509002:0:6',
157
- to: '8509073:0:2',
256
+ from: { name: 'Landquart', platform: '6' },
257
+ to: { name: 'Davos Platz', platform: '2' },
158
258
  departure: '11:20',
159
259
  arrival: '12:27',
160
260
  route: { type: 'RAIL', name: 'RE13' },
@@ -162,123 +262,357 @@ const routes = [
162
262
  ],
163
263
  },
164
264
  {
165
- from: 'Parent8504100',
166
- to: 'Parent8504749',
265
+ from: 'Fribourg/Freiburg',
266
+ to: 'Plan-Francey',
167
267
  at: '09:00',
168
- route: [
268
+ legs: [
169
269
  {
170
- from: '8504100:0:2',
171
- to: '8504086:0:2',
270
+ from: { name: 'Fribourg/Freiburg', platform: '2' },
271
+ to: { name: 'Bulle', platform: '2' },
172
272
  departure: '09:04',
173
273
  arrival: '09:40',
174
- route: {
175
- type: 'RAIL',
176
- name: 'RE3',
177
- },
274
+ route: { type: 'RAIL', name: 'RE3' },
178
275
  },
179
276
  {
180
- from: '8504086:0:2',
181
- to: '8504086:0:4',
277
+ from: { name: 'Bulle', platform: '2' },
278
+ to: { name: 'Bulle', platform: '4' },
182
279
  type: 'REQUIRES_MINIMAL_TIME',
183
280
  minTransferTime: 3,
184
281
  },
185
282
  {
186
- from: '8504086:0:4',
187
- to: '8504077:0:2',
283
+ from: { name: 'Bulle', platform: '4' },
284
+ to: { name: 'Gruyères', platform: '2' },
188
285
  departure: '09:50',
189
286
  arrival: '09:57',
190
- route: {
191
- type: 'RAIL',
192
- name: 'S50',
193
- },
287
+ route: { type: 'RAIL', name: 'S50' },
194
288
  },
195
289
  {
196
- from: '8504077:0:2',
197
- to: '8577737:0:B',
290
+ from: { name: 'Gruyères', platform: '2' },
291
+ to: { name: 'Gruyères, gare', platform: 'B' },
198
292
  type: 'REQUIRES_MINIMAL_TIME',
199
293
  minTransferTime: 2,
200
294
  },
201
295
  {
202
- from: '8577737:0:B',
203
- to: '8504880:0:10000',
296
+ from: { name: 'Gruyères, gare', platform: 'B' },
297
+ to: { name: 'Moléson-sur-Gruyères' },
204
298
  departure: '10:33',
205
299
  arrival: '10:44',
206
- route: {
207
- type: 'BUS',
208
- name: '263',
209
- },
300
+ route: { type: 'BUS', name: '263' },
210
301
  },
211
302
  {
212
- from: '8504880:0:10000',
213
- to: '8530024',
303
+ from: { name: 'Moléson-sur-Gruyères' },
304
+ to: { name: 'Moléson-sur-Gruyères (funi)' },
214
305
  type: 'REQUIRES_MINIMAL_TIME',
215
306
  minTransferTime: 2,
216
307
  },
217
308
  {
218
- from: '8530024',
219
- to: '8504749',
309
+ from: { name: 'Moléson-sur-Gruyères (funi)' },
310
+ to: { name: 'Plan-Francey' },
220
311
  departure: '11:00',
221
312
  arrival: '11:05',
222
- route: {
223
- type: 'FUNICULAR',
224
- name: 'FUN',
225
- },
313
+ route: { type: 'FUNICULAR', name: 'FUN' },
226
314
  },
227
315
  ],
228
316
  },
229
317
  ];
230
318
 
231
- const stopsPath = new URL('./timetable/stops.bin', import.meta.url).pathname;
232
- const timetablePath = new URL('./timetable/timetable.bin', import.meta.url)
233
- .pathname;
319
+ const rangeRoutes: {
320
+ from: string;
321
+ to: string;
322
+ earliest: string;
323
+ latest: string;
324
+ // Runs are listed latest-departure-first, matching Range RAPTOR's natural order.
325
+ runs: { departureTime: string; legs: ExpectedLeg[] }[];
326
+ }[] = [
327
+ {
328
+ from: 'Fribourg/Freiburg',
329
+ to: 'Moléson-sur-Gruyères',
330
+ earliest: '08:00',
331
+ latest: '10:00',
332
+ runs: [
333
+ {
334
+ departureTime: '09:34',
335
+ legs: [
336
+ {
337
+ from: { name: 'Fribourg/Freiburg', platform: '2' },
338
+ to: { name: 'Bulle', platform: '2' },
339
+ departure: '09:34',
340
+ arrival: '10:10',
341
+ route: { type: 'RAIL', name: 'RE2' },
342
+ },
343
+ {
344
+ from: { name: 'Bulle', platform: '2' },
345
+ to: { name: 'Bulle', platform: '4' },
346
+ type: 'REQUIRES_MINIMAL_TIME',
347
+ minTransferTime: 3,
348
+ },
349
+ {
350
+ from: { name: 'Bulle', platform: '4' },
351
+ to: { name: 'Gruyères', platform: '1' },
352
+ departure: '10:20',
353
+ arrival: '10:28',
354
+ route: { type: 'RAIL', name: 'S51' },
355
+ },
356
+ {
357
+ from: { name: 'Gruyères', platform: '1' },
358
+ to: { name: 'Gruyères, gare', platform: 'B' },
359
+ type: 'REQUIRES_MINIMAL_TIME',
360
+ minTransferTime: 2,
361
+ },
362
+ {
363
+ from: { name: 'Gruyères, gare', platform: 'B' },
364
+ to: { name: 'Moléson-sur-Gruyères' },
365
+ departure: '10:33',
366
+ arrival: '10:44',
367
+ route: { type: 'BUS', name: '263' },
368
+ },
369
+ ],
370
+ },
371
+ {
372
+ departureTime: '08:34',
373
+ legs: [
374
+ {
375
+ from: { name: 'Fribourg/Freiburg', platform: '2' },
376
+ to: { name: 'Bulle', platform: '2' },
377
+ departure: '08:34',
378
+ arrival: '09:10',
379
+ route: { type: 'RAIL', name: 'RE2' },
380
+ },
381
+ {
382
+ from: { name: 'Bulle', platform: '2' },
383
+ to: { name: 'Bulle', platform: '4' },
384
+ type: 'REQUIRES_MINIMAL_TIME',
385
+ minTransferTime: 3,
386
+ },
387
+ {
388
+ from: { name: 'Bulle', platform: '4' },
389
+ to: { name: 'Gruyères', platform: '1' },
390
+ departure: '09:20',
391
+ arrival: '09:28',
392
+ route: { type: 'RAIL', name: 'S51' },
393
+ },
394
+ {
395
+ from: { name: 'Gruyères', platform: '1' },
396
+ to: { name: 'Gruyères, gare', platform: 'B' },
397
+ type: 'REQUIRES_MINIMAL_TIME',
398
+ minTransferTime: 2,
399
+ },
400
+ {
401
+ from: { name: 'Gruyères, gare', platform: 'B' },
402
+ to: { name: 'Moléson-sur-Gruyères' },
403
+ departure: '09:33',
404
+ arrival: '09:44',
405
+ route: { type: 'BUS', name: '263' },
406
+ },
407
+ ],
408
+ },
409
+ ],
410
+ },
411
+ {
412
+ from: 'Basel SBB',
413
+ to: 'Strasbourg',
414
+ earliest: '16:00',
415
+ latest: '18:00',
416
+ runs: [
417
+ {
418
+ departureTime: '17:21',
419
+ legs: [
420
+ {
421
+ from: { name: 'Basel SBB', platform: '31' },
422
+ to: { name: 'Strasbourg' },
423
+ departure: '17:21',
424
+ arrival: '18:39',
425
+ route: { type: 'RAIL', name: 'TER' },
426
+ },
427
+ ],
428
+ },
429
+ {
430
+ departureTime: '16:38',
431
+ legs: [
432
+ {
433
+ from: { name: 'Basel SBB', platform: '33' },
434
+ to: { name: 'Saint-Louis (Haut-Rhin)' },
435
+ departure: '16:38',
436
+ arrival: '16:46',
437
+ route: { type: 'RAIL', name: 'TER' },
438
+ },
439
+ {
440
+ from: { name: 'Saint-Louis (Haut-Rhin)' },
441
+ to: { name: 'Strasbourg' },
442
+ departure: '16:54',
443
+ arrival: '18:09',
444
+ route: { type: 'RAIL', name: 'K200' },
445
+ },
446
+ ],
447
+ },
448
+ ],
449
+ },
450
+ {
451
+ from: 'Fribourg/Freiburg',
452
+ to: 'Davos Platz',
453
+ earliest: '08:00',
454
+ latest: '09:00',
455
+ runs: [
456
+ {
457
+ departureTime: '08:33',
458
+ legs: [
459
+ {
460
+ from: { name: 'Fribourg/Freiburg', platform: '3' },
461
+ to: { name: 'Bern', platform: '10' },
462
+ departure: '08:33',
463
+ arrival: '08:56',
464
+ route: { type: 'RAIL', name: 'IR15' },
465
+ },
466
+ {
467
+ from: { name: 'Bern', platform: '10' },
468
+ to: { name: 'Bern', platform: '2' },
469
+ type: 'REQUIRES_MINIMAL_TIME',
470
+ minTransferTime: 6,
471
+ },
472
+ {
473
+ from: { name: 'Bern', platform: '2' },
474
+ to: { name: 'Zürich HB', platform: '34' },
475
+ departure: '09:02',
476
+ arrival: '09:58',
477
+ route: { type: 'RAIL', name: 'IC81' },
478
+ },
479
+ {
480
+ from: { name: 'Zürich HB', platform: '34' },
481
+ to: { name: 'Zürich HB', platform: '10' },
482
+ type: 'REQUIRES_MINIMAL_TIME',
483
+ minTransferTime: 7,
484
+ },
485
+ {
486
+ from: { name: 'Zürich HB', platform: '10' },
487
+ to: { name: 'Landquart', platform: '2' },
488
+ departure: '10:07',
489
+ arrival: '11:11',
490
+ route: { type: 'RAIL', name: 'IC3' },
491
+ },
492
+ {
493
+ from: { name: 'Landquart', platform: '2' },
494
+ to: { name: 'Landquart', platform: '6' },
495
+ type: 'REQUIRES_MINIMAL_TIME',
496
+ minTransferTime: 4,
497
+ },
498
+ {
499
+ from: { name: 'Landquart', platform: '6' },
500
+ to: { name: 'Davos Platz', platform: '2' },
501
+ departure: '11:20',
502
+ arrival: '12:27',
503
+ route: { type: 'RAIL', name: 'RE13' },
504
+ },
505
+ ],
506
+ },
507
+ {
508
+ departureTime: '08:03',
509
+ legs: [
510
+ {
511
+ from: { name: 'Fribourg/Freiburg', platform: '3' },
512
+ to: { name: 'Zürich HB', platform: '33' },
513
+ departure: '08:03',
514
+ arrival: '09:28',
515
+ route: { type: 'RAIL', name: 'IC1' },
516
+ },
517
+ {
518
+ from: { name: 'Zürich HB', platform: '33' },
519
+ to: { name: 'Zürich HB', platform: '4' },
520
+ type: 'REQUIRES_MINIMAL_TIME',
521
+ minTransferTime: 7,
522
+ },
523
+ {
524
+ from: { name: 'Zürich HB', platform: '4' },
525
+ to: { name: 'Landquart', platform: '2' },
526
+ departure: '09:38',
527
+ arrival: '10:41',
528
+ route: { type: 'RAIL', name: 'IC3' },
529
+ },
530
+ {
531
+ from: { name: 'Landquart', platform: '2' },
532
+ to: { name: 'Landquart', platform: '6' },
533
+ type: 'REQUIRES_MINIMAL_TIME',
534
+ minTransferTime: 4,
535
+ },
536
+ {
537
+ from: { name: 'Landquart', platform: '6' },
538
+ to: { name: 'Davos Platz', platform: '1' },
539
+ departure: '10:49',
540
+ arrival: '12:03',
541
+ route: { type: 'RAIL', name: 'RE24' },
542
+ },
543
+ ],
544
+ },
545
+ ],
546
+ },
547
+ ];
234
548
 
235
549
  describe('E2E Tests for Transit Router', () => {
236
- const stopsIndex = StopsIndex.fromData(fs.readFileSync(stopsPath));
237
- const timetable = Timetable.fromData(fs.readFileSync(timetablePath));
550
+ describe('point queries', () => {
551
+ for (const { from, to, at, legs } of pointRoutes) {
552
+ it(`${from} → ${to} at ${at}`, () => {
553
+ const fromStation = findStation(from);
554
+ const toStation = findStation(to);
238
555
 
239
- const router = new Router(timetable, stopsIndex);
556
+ const result = router.route(
557
+ new Query.Builder()
558
+ .from(fromStation.id)
559
+ .to(toStation.id)
560
+ .departureTime(timeFromString(at))
561
+ .maxTransfers(5)
562
+ .build(),
563
+ );
240
564
 
241
- routes.forEach(({ from, to, at, route }) => {
242
- it(`Route from ${from} to ${to} at ${at}`, () => {
243
- const fromStop = stopsIndex.findStopBySourceStopId(from);
244
- const toStop = stopsIndex.findStopBySourceStopId(to);
565
+ const bestRoute = result.bestRoute(toStation.id);
566
+ assert.ok(bestRoute, 'No route found');
567
+ assert.deepStrictEqual(toExpectedLegs(bestRoute), legs);
568
+ });
569
+ }
570
+ });
245
571
 
246
- assert.ok(fromStop, `Stop not found: ${from}`);
247
- assert.ok(toStop, `Stop not found: ${to}`);
572
+ describe('range queries', () => {
573
+ for (const { from, to, earliest, latest, runs } of rangeRoutes) {
574
+ it(`${from} → ${to} [${earliest}–${latest}]`, () => {
575
+ const fromStation = findStation(from);
576
+ const toStation = findStation(to);
248
577
 
249
- const departureTime = timeFromString(at);
578
+ const result = router.rangeRoute(
579
+ new RangeQuery.Builder()
580
+ .from(fromStation.id)
581
+ .to(toStation.id)
582
+ .departureTime(timeFromString(earliest))
583
+ .lastDepartureTime(timeFromString(latest))
584
+ .maxTransfers(5)
585
+ .build(),
586
+ );
250
587
 
251
- const queryObject = new Query.Builder()
252
- .from(fromStop.id)
253
- .to(toStop.id)
254
- .departureTime(departureTime)
255
- .maxTransfers(5)
256
- .build();
588
+ assert.strictEqual(
589
+ result.size,
590
+ runs.length,
591
+ `Expected ${runs.length} Pareto-optimal run(s), got ${result.size}`,
592
+ );
257
593
 
258
- const result = router.route(queryObject);
259
- const bestRoute = result.bestRoute(toStop.id);
260
- console.log();
261
- assert.ok(bestRoute, 'No route found');
262
- const actualRoute = bestRoute.asJson();
263
- const actualRouteWithSourceIds = actualRoute.map((segment) => {
264
- const fromStop = stopsIndex.findStopById(segment.from);
265
- const toStop = stopsIndex.findStopById(segment.to);
594
+ const actualRuns = [...result];
595
+ for (let i = 0; i < runs.length; i++) {
596
+ const actualRun = actualRuns[i];
597
+ const expectedRun = runs[i];
598
+ assert.ok(actualRun, `Run ${i}: missing actual run`);
599
+ assert.ok(expectedRun, `Run ${i}: missing expected run`);
266
600
 
267
- assert.ok(fromStop?.sourceStopId, `Stop not found: ${segment.from}`);
268
- assert.ok(toStop?.sourceStopId, `Stop not found: ${segment.to}`);
601
+ assert.strictEqual(
602
+ timeToString(actualRun.departureTime),
603
+ expectedRun.departureTime,
604
+ `Run ${i}: departure time mismatch`,
605
+ );
269
606
 
270
- return {
271
- ...segment,
272
- from: fromStop.sourceStopId,
273
- to: toStop.sourceStopId,
274
- };
607
+ const route = actualRun.result.bestRoute(toStation.id);
608
+ assert.ok(route, `Run ${i}: no route found`);
609
+ assert.deepStrictEqual(
610
+ toExpectedLegs(route),
611
+ expectedRun.legs,
612
+ `Run ${i}: legs mismatch`,
613
+ );
614
+ }
275
615
  });
276
-
277
- assert.deepStrictEqual(
278
- actualRouteWithSourceIds,
279
- route,
280
- `Route mismatch for query from ${from} to ${to} at ${at}`,
281
- );
282
- });
616
+ }
283
617
  });
284
618
  });