minotor 3.0.1 → 4.0.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.
- package/.cspell.json +12 -1
- package/.gitattributes +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +3 -0
- package/.github/workflows/minotor.yml +17 -1
- package/CHANGELOG.md +8 -3
- package/README.md +35 -14
- package/dist/__e2e__/router.test.d.ts +1 -0
- package/dist/cli/perf.d.ts +28 -0
- package/dist/cli/utils.d.ts +6 -2
- package/dist/cli.mjs +2130 -909
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +7 -1
- package/dist/gtfs/utils.d.ts +1 -1
- package/dist/parser.cjs.js +1236 -755
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.d.ts +4 -2
- package/dist/parser.esm.js +1236 -755
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/result.test.d.ts +1 -0
- package/dist/routing/query.d.ts +27 -6
- package/dist/routing/result.d.ts +1 -1
- package/dist/routing/route.d.ts +47 -2
- package/dist/routing/router.d.ts +15 -1
- package/dist/stops/stopsIndex.d.ts +3 -3
- package/dist/timetable/__tests__/route.test.d.ts +1 -0
- package/dist/timetable/__tests__/time.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +7 -1
- package/dist/timetable/proto/timetable.d.ts +3 -2
- package/dist/timetable/route.d.ts +157 -0
- package/dist/timetable/time.d.ts +21 -0
- package/dist/timetable/timetable.d.ts +42 -62
- package/package.json +36 -34
- package/src/__e2e__/benchmark.json +22 -0
- package/src/__e2e__/router.test.ts +209 -0
- package/src/__e2e__/timetable/stops.bin +3 -0
- package/src/__e2e__/timetable/timetable.bin +3 -0
- package/src/cli/minotor.ts +51 -1
- package/src/cli/perf.ts +136 -0
- package/src/cli/repl.ts +25 -13
- package/src/cli/utils.ts +6 -28
- package/src/gtfs/__tests__/parser.test.ts +12 -15
- package/src/gtfs/__tests__/services.test.ts +1 -0
- package/src/gtfs/__tests__/transfers.test.ts +0 -1
- package/src/gtfs/__tests__/trips.test.ts +56 -74
- package/src/gtfs/parser.ts +49 -9
- package/src/gtfs/profiles/ch.ts +1 -1
- package/src/gtfs/routes.ts +4 -4
- package/src/gtfs/services.ts +15 -2
- package/src/gtfs/stops.ts +7 -3
- package/src/gtfs/transfers.ts +6 -3
- package/src/gtfs/trips.ts +206 -108
- package/src/gtfs/utils.ts +13 -2
- package/src/parser.ts +4 -2
- package/src/router.ts +17 -11
- package/src/routing/__tests__/result.test.ts +392 -0
- package/src/routing/__tests__/router.test.ts +94 -137
- package/src/routing/query.ts +28 -7
- package/src/routing/result.ts +10 -5
- package/src/routing/route.ts +95 -9
- package/src/routing/router.ts +82 -66
- package/src/stops/__tests__/io.test.ts +1 -1
- package/src/stops/__tests__/stopFinder.test.ts +1 -1
- package/src/stops/proto/stops.ts +4 -4
- package/src/stops/stopsIndex.ts +3 -3
- package/src/timetable/__tests__/io.test.ts +16 -23
- package/src/timetable/__tests__/route.test.ts +325 -0
- package/src/timetable/__tests__/time.test.ts +494 -0
- package/src/timetable/__tests__/timetable.test.ts +60 -75
- package/src/timetable/io.ts +32 -26
- package/src/timetable/proto/timetable.proto +3 -2
- package/src/timetable/proto/timetable.ts +15 -14
- package/src/timetable/route.ts +361 -0
- package/src/timetable/time.ts +40 -8
- package/src/timetable/timetable.ts +75 -166
- package/tsconfig.build.json +1 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { Duration } from '../duration.js';
|
|
5
|
+
import { Time } from '../time.js';
|
|
6
|
+
|
|
7
|
+
describe('Time', () => {
|
|
8
|
+
describe('Static factory methods', () => {
|
|
9
|
+
describe('infinity()', () => {
|
|
10
|
+
it('should return a Time instance representing infinity', () => {
|
|
11
|
+
const infinityTime = Time.infinity();
|
|
12
|
+
assert.strictEqual(infinityTime.toMinutes(), Number.MAX_SAFE_INTEGER);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return the same infinity value for multiple calls', () => {
|
|
16
|
+
const infinity1 = Time.infinity();
|
|
17
|
+
const infinity2 = Time.infinity();
|
|
18
|
+
assert(infinity1.equals(infinity2));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('origin()', () => {
|
|
23
|
+
it('should return a Time instance representing midnight (0 minutes)', () => {
|
|
24
|
+
const midnight = Time.origin();
|
|
25
|
+
assert.strictEqual(midnight.toMinutes(), 0);
|
|
26
|
+
assert.strictEqual(midnight.toString(), '00:00');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('fromMinutes()', () => {
|
|
31
|
+
it('should create a Time instance from minutes', () => {
|
|
32
|
+
const time = Time.fromMinutes(120);
|
|
33
|
+
assert.strictEqual(time.toMinutes(), 120);
|
|
34
|
+
assert.strictEqual(time.toString(), '02:00');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle zero minutes', () => {
|
|
38
|
+
const time = Time.fromMinutes(0);
|
|
39
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
40
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle minutes beyond 24 hours', () => {
|
|
44
|
+
const time = Time.fromMinutes(1500); // 25 hours
|
|
45
|
+
assert.strictEqual(time.toMinutes(), 1500);
|
|
46
|
+
assert.strictEqual(time.toString(), '01:00'); // wraps around to next day
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('fromHMS()', () => {
|
|
51
|
+
it('should create a Time instance from hours, minutes, and seconds', () => {
|
|
52
|
+
const time = Time.fromHMS(14, 30, 45);
|
|
53
|
+
assert.strictEqual(time.toMinutes(), 14 * 60 + 31); // rounds 30:45 to 31 minutes
|
|
54
|
+
assert.strictEqual(time.toString(), '14:31');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should round seconds to the nearest minute', () => {
|
|
58
|
+
const time1 = Time.fromHMS(10, 30, 29); // rounds down
|
|
59
|
+
assert.strictEqual(time1.toMinutes(), 10 * 60 + 30);
|
|
60
|
+
|
|
61
|
+
const time2 = Time.fromHMS(10, 30, 30); // rounds up
|
|
62
|
+
assert.strictEqual(time2.toMinutes(), 10 * 60 + 31);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle midnight', () => {
|
|
66
|
+
const time = Time.fromHMS(0, 0, 0);
|
|
67
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
68
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw error for negative values', () => {
|
|
72
|
+
assert.throws(() => Time.fromHMS(-1, 0, 0), /Invalid time/);
|
|
73
|
+
assert.throws(() => Time.fromHMS(0, -1, 0), /Invalid time/);
|
|
74
|
+
assert.throws(() => Time.fromHMS(0, 0, -1), /Invalid time/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw error for invalid minute values', () => {
|
|
78
|
+
assert.throws(() => Time.fromHMS(10, 60, 0), /Invalid time/);
|
|
79
|
+
assert.throws(() => Time.fromHMS(10, 65, 0), /Invalid time/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw error for invalid second values', () => {
|
|
83
|
+
assert.throws(() => Time.fromHMS(10, 30, 60), /Invalid time/);
|
|
84
|
+
assert.throws(() => Time.fromHMS(10, 30, 75), /Invalid time/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('fromHM()', () => {
|
|
89
|
+
it('should create a Time instance from hours and minutes', () => {
|
|
90
|
+
const time = Time.fromHM(15, 45);
|
|
91
|
+
assert.strictEqual(time.toMinutes(), 15 * 60 + 45);
|
|
92
|
+
assert.strictEqual(time.toString(), '15:45');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle midnight', () => {
|
|
96
|
+
const time = Time.fromHM(0, 0);
|
|
97
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
98
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw error for negative hours', () => {
|
|
102
|
+
assert.throws(() => Time.fromHM(-1, 30), /Invalid time/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw error for negative minutes', () => {
|
|
106
|
+
assert.throws(() => Time.fromHM(10, -5), /Invalid time/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw error for invalid minute values', () => {
|
|
110
|
+
assert.throws(() => Time.fromHM(10, 60), /Invalid time/);
|
|
111
|
+
assert.throws(() => Time.fromHM(10, 75), /Invalid time/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('fromDate()', () => {
|
|
116
|
+
it('should create a Time instance from a Date object', () => {
|
|
117
|
+
const date = new Date(2023, 5, 15, 14, 30, 45);
|
|
118
|
+
const time = Time.fromDate(date);
|
|
119
|
+
assert.strictEqual(time.toMinutes(), 14 * 60 + 31); // rounds seconds
|
|
120
|
+
assert.strictEqual(time.toString(), '14:31');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should handle midnight date', () => {
|
|
124
|
+
const date = new Date(2023, 5, 15, 0, 0, 0);
|
|
125
|
+
const time = Time.fromDate(date);
|
|
126
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
127
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle date near end of day', () => {
|
|
131
|
+
const date = new Date(2023, 5, 15, 23, 59, 30);
|
|
132
|
+
const time = Time.fromDate(date);
|
|
133
|
+
assert.strictEqual(time.toMinutes(), 24 * 60); // rounds up to next day
|
|
134
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('fromString()', () => {
|
|
139
|
+
it('should parse HH:MM format', () => {
|
|
140
|
+
const time = Time.fromString('14:30');
|
|
141
|
+
assert.strictEqual(time.toMinutes(), 14 * 60 + 30);
|
|
142
|
+
assert.strictEqual(time.toString(), '14:30');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should parse HH:MM:SS format', () => {
|
|
146
|
+
const time = Time.fromString('14:30:45');
|
|
147
|
+
assert.strictEqual(time.toMinutes(), 14 * 60 + 31); // rounds seconds
|
|
148
|
+
assert.strictEqual(time.toString(), '14:31');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle midnight', () => {
|
|
152
|
+
const time = Time.fromString('00:00');
|
|
153
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
154
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle single digit hours and minutes', () => {
|
|
158
|
+
const time = Time.fromString('9:05');
|
|
159
|
+
assert.strictEqual(time.toMinutes(), 9 * 60 + 5);
|
|
160
|
+
assert.strictEqual(time.toString(), '09:05');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should throw error for invalid format', () => {
|
|
164
|
+
assert.throws(
|
|
165
|
+
() => Time.fromString('invalid'),
|
|
166
|
+
/Input string must be in the format/,
|
|
167
|
+
);
|
|
168
|
+
assert.throws(() => Time.fromString('12:65'), /Invalid time/);
|
|
169
|
+
assert.throws(() => Time.fromString('12:30:65'), /Invalid time/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should throw error for missing components', () => {
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => Time.fromString('14'),
|
|
175
|
+
/Input string must be in the format/,
|
|
176
|
+
);
|
|
177
|
+
assert.throws(
|
|
178
|
+
() => Time.fromString('14:'),
|
|
179
|
+
/Input string must be in the format/,
|
|
180
|
+
);
|
|
181
|
+
assert.throws(
|
|
182
|
+
() => Time.fromString(':30'),
|
|
183
|
+
/Input string must be in the format/,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should throw error for non-numeric values', () => {
|
|
188
|
+
assert.throws(
|
|
189
|
+
() => Time.fromString('ab:cd'),
|
|
190
|
+
/Input string must be in the format/,
|
|
191
|
+
);
|
|
192
|
+
assert.throws(
|
|
193
|
+
() => Time.fromString('12:ab'),
|
|
194
|
+
/Input string must be in the format/,
|
|
195
|
+
);
|
|
196
|
+
assert.throws(
|
|
197
|
+
() => Time.fromString('12:30:ab'),
|
|
198
|
+
/Input string must be in the format/,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Instance methods', () => {
|
|
205
|
+
describe('toString()', () => {
|
|
206
|
+
it('should format time as HH:MM', () => {
|
|
207
|
+
const time = Time.fromMinutes(14 * 60 + 30);
|
|
208
|
+
assert.strictEqual(time.toString(), '14:30');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should pad single digits with zeros', () => {
|
|
212
|
+
const time = Time.fromMinutes(9 * 60 + 5);
|
|
213
|
+
assert.strictEqual(time.toString(), '09:05');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle midnight', () => {
|
|
217
|
+
const time = Time.fromMinutes(0);
|
|
218
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should wrap hours beyond 24', () => {
|
|
222
|
+
const time = Time.fromMinutes(25 * 60 + 30); // 25:30
|
|
223
|
+
assert.strictEqual(time.toString(), '01:30');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle exactly 24 hours', () => {
|
|
227
|
+
const time = Time.fromMinutes(24 * 60);
|
|
228
|
+
assert.strictEqual(time.toString(), '00:00');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('toMinutes()', () => {
|
|
233
|
+
it('should return the minutes since midnight', () => {
|
|
234
|
+
const time = Time.fromMinutes(150);
|
|
235
|
+
assert.strictEqual(time.toMinutes(), 150);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should return 0 for midnight', () => {
|
|
239
|
+
const time = Time.origin();
|
|
240
|
+
assert.strictEqual(time.toMinutes(), 0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('plus()', () => {
|
|
245
|
+
it('should add duration to time', () => {
|
|
246
|
+
const time = Time.fromMinutes(120); // 02:00
|
|
247
|
+
const duration = Duration.fromMinutes(30);
|
|
248
|
+
const result = time.plus(duration);
|
|
249
|
+
assert.strictEqual(result.toMinutes(), 150); // 02:30
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle adding duration with seconds', () => {
|
|
253
|
+
const time = Time.fromMinutes(120);
|
|
254
|
+
const duration = Duration.fromSeconds(90); // 1.5 minutes
|
|
255
|
+
const result = time.plus(duration);
|
|
256
|
+
assert.strictEqual(result.toMinutes(), 122); // rounds to nearest minute
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should not modify original time', () => {
|
|
260
|
+
const time = Time.fromMinutes(120);
|
|
261
|
+
const duration = Duration.fromMinutes(30);
|
|
262
|
+
time.plus(duration);
|
|
263
|
+
assert.strictEqual(time.toMinutes(), 120); // original unchanged
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('minus()', () => {
|
|
268
|
+
it('should subtract duration from time', () => {
|
|
269
|
+
const time = Time.fromMinutes(150); // 02:30
|
|
270
|
+
const duration = Duration.fromMinutes(30);
|
|
271
|
+
const result = time.minus(duration);
|
|
272
|
+
assert.strictEqual(result.toMinutes(), 120); // 02:00
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should handle subtracting duration with seconds', () => {
|
|
276
|
+
const time = Time.fromMinutes(150);
|
|
277
|
+
const duration = Duration.fromSeconds(90); // 1.5 minutes
|
|
278
|
+
const result = time.minus(duration);
|
|
279
|
+
assert.strictEqual(result.toMinutes(), 149); // rounds to nearest minute
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should wrap to previous day for negative results', () => {
|
|
283
|
+
const time = Time.fromMinutes(30); // 00:30
|
|
284
|
+
const duration = Duration.fromMinutes(60); // 1 hour
|
|
285
|
+
const result = time.minus(duration);
|
|
286
|
+
assert.strictEqual(result.toMinutes(), 23 * 60 + 30); // 23:30 previous day
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should not modify original time', () => {
|
|
290
|
+
const time = Time.fromMinutes(150);
|
|
291
|
+
const duration = Duration.fromMinutes(30);
|
|
292
|
+
time.minus(duration);
|
|
293
|
+
assert.strictEqual(time.toMinutes(), 150); // original unchanged
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('diff()', () => {
|
|
298
|
+
it('should return absolute difference between times', () => {
|
|
299
|
+
const time1 = Time.fromMinutes(150); // 02:30
|
|
300
|
+
const time2 = Time.fromMinutes(120); // 02:00
|
|
301
|
+
const diff = time1.diff(time2);
|
|
302
|
+
assert.strictEqual(diff.toSeconds(), 30 * 60); // 30 minutes
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should return absolute difference regardless of order', () => {
|
|
306
|
+
const time1 = Time.fromMinutes(120); // 02:00
|
|
307
|
+
const time2 = Time.fromMinutes(150); // 02:30
|
|
308
|
+
const diff = time1.diff(time2);
|
|
309
|
+
assert.strictEqual(diff.toSeconds(), 30 * 60); // 30 minutes
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should return zero for same times', () => {
|
|
313
|
+
const time = Time.fromMinutes(120);
|
|
314
|
+
const diff = time.diff(time);
|
|
315
|
+
assert.strictEqual(diff.toSeconds(), 0);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('Comparison methods', () => {
|
|
320
|
+
describe('isAfter()', () => {
|
|
321
|
+
it('should return true when time is after other time', () => {
|
|
322
|
+
const time1 = Time.fromMinutes(150);
|
|
323
|
+
const time2 = Time.fromMinutes(120);
|
|
324
|
+
assert.strictEqual(time1.isAfter(time2), true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should return false when time is before other time', () => {
|
|
328
|
+
const time1 = Time.fromMinutes(120);
|
|
329
|
+
const time2 = Time.fromMinutes(150);
|
|
330
|
+
assert.strictEqual(time1.isAfter(time2), false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should return false when times are equal', () => {
|
|
334
|
+
const time1 = Time.fromMinutes(120);
|
|
335
|
+
const time2 = Time.fromMinutes(120);
|
|
336
|
+
assert.strictEqual(time1.isAfter(time2), false);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('isBefore()', () => {
|
|
341
|
+
it('should return true when time is before other time', () => {
|
|
342
|
+
const time1 = Time.fromMinutes(120);
|
|
343
|
+
const time2 = Time.fromMinutes(150);
|
|
344
|
+
assert.strictEqual(time1.isBefore(time2), true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should return false when time is after other time', () => {
|
|
348
|
+
const time1 = Time.fromMinutes(150);
|
|
349
|
+
const time2 = Time.fromMinutes(120);
|
|
350
|
+
assert.strictEqual(time1.isBefore(time2), false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should return false when times are equal', () => {
|
|
354
|
+
const time1 = Time.fromMinutes(120);
|
|
355
|
+
const time2 = Time.fromMinutes(120);
|
|
356
|
+
assert.strictEqual(time1.isBefore(time2), false);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('equals()', () => {
|
|
361
|
+
it('should return true when times are equal', () => {
|
|
362
|
+
const time1 = Time.fromMinutes(120);
|
|
363
|
+
const time2 = Time.fromMinutes(120);
|
|
364
|
+
assert.strictEqual(time1.equals(time2), true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should return false when times are different', () => {
|
|
368
|
+
const time1 = Time.fromMinutes(120);
|
|
369
|
+
const time2 = Time.fromMinutes(150);
|
|
370
|
+
assert.strictEqual(time1.equals(time2), false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should work with times created differently but representing same time', () => {
|
|
374
|
+
const time1 = Time.fromHM(2, 30);
|
|
375
|
+
const time2 = Time.fromMinutes(150);
|
|
376
|
+
assert.strictEqual(time1.equals(time2), true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('Static utility methods', () => {
|
|
383
|
+
describe('max()', () => {
|
|
384
|
+
it('should return the maximum time from multiple times', () => {
|
|
385
|
+
const time1 = Time.fromMinutes(120);
|
|
386
|
+
const time2 = Time.fromMinutes(180);
|
|
387
|
+
const time3 = Time.fromMinutes(90);
|
|
388
|
+
const maxTime = Time.max(time1, time2, time3);
|
|
389
|
+
assert.strictEqual(maxTime.toMinutes(), 180);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should work with single time', () => {
|
|
393
|
+
const time = Time.fromMinutes(120);
|
|
394
|
+
const maxTime = Time.max(time);
|
|
395
|
+
assert.strictEqual(maxTime.toMinutes(), 120);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should work with duplicate times', () => {
|
|
399
|
+
const time1 = Time.fromMinutes(120);
|
|
400
|
+
const time2 = Time.fromMinutes(120);
|
|
401
|
+
const maxTime = Time.max(time1, time2);
|
|
402
|
+
assert.strictEqual(maxTime.toMinutes(), 120);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should throw error for empty array', () => {
|
|
406
|
+
assert.throws(
|
|
407
|
+
() => Time.max(),
|
|
408
|
+
/At least one Time instance is required/,
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should handle infinity time', () => {
|
|
413
|
+
const time1 = Time.fromMinutes(120);
|
|
414
|
+
const infinity = Time.infinity();
|
|
415
|
+
const maxTime = Time.max(time1, infinity);
|
|
416
|
+
assert.strictEqual(maxTime, infinity);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe('min()', () => {
|
|
421
|
+
it('should return the minimum time from multiple times', () => {
|
|
422
|
+
const time1 = Time.fromMinutes(120);
|
|
423
|
+
const time2 = Time.fromMinutes(180);
|
|
424
|
+
const time3 = Time.fromMinutes(90);
|
|
425
|
+
const minTime = Time.min(time1, time2, time3);
|
|
426
|
+
assert.strictEqual(minTime.toMinutes(), 90);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should work with single time', () => {
|
|
430
|
+
const time = Time.fromMinutes(120);
|
|
431
|
+
const minTime = Time.min(time);
|
|
432
|
+
assert.strictEqual(minTime.toMinutes(), 120);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should work with duplicate times', () => {
|
|
436
|
+
const time1 = Time.fromMinutes(120);
|
|
437
|
+
const time2 = Time.fromMinutes(120);
|
|
438
|
+
const minTime = Time.min(time1, time2);
|
|
439
|
+
assert.strictEqual(minTime.toMinutes(), 120);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should throw error for empty array', () => {
|
|
443
|
+
assert.throws(
|
|
444
|
+
() => Time.min(),
|
|
445
|
+
/At least one Time instance is required/,
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should handle origin time', () => {
|
|
450
|
+
const time1 = Time.fromMinutes(120);
|
|
451
|
+
const origin = Time.origin();
|
|
452
|
+
const minTime = Time.min(time1, origin);
|
|
453
|
+
assert.strictEqual(minTime, origin);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('Edge cases and special scenarios', () => {
|
|
459
|
+
it('should handle times beyond 24 hours correctly', () => {
|
|
460
|
+
const time = Time.fromMinutes(25 * 60); // 25:00
|
|
461
|
+
assert.strictEqual(time.toMinutes(), 25 * 60);
|
|
462
|
+
assert.strictEqual(time.toString(), '01:00'); // wraps to next day for display
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should handle very large time values', () => {
|
|
466
|
+
const largeTime = Time.fromMinutes(1000000);
|
|
467
|
+
assert.strictEqual(largeTime.toMinutes(), 1000000);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should maintain precision with rounding', () => {
|
|
471
|
+
const time = Time.fromHMS(10, 30, 29); // should round down
|
|
472
|
+
assert.strictEqual(time.toMinutes(), 10 * 60 + 30);
|
|
473
|
+
|
|
474
|
+
const time2 = Time.fromHMS(10, 30, 31); // should round up
|
|
475
|
+
assert.strictEqual(time2.toMinutes(), 10 * 60 + 31);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should work with chained operations', () => {
|
|
479
|
+
const time = Time.fromHM(10, 0)
|
|
480
|
+
.plus(Duration.fromMinutes(30))
|
|
481
|
+
.minus(Duration.fromMinutes(15));
|
|
482
|
+
assert.strictEqual(time.toMinutes(), 10 * 60 + 15);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should handle comparison with infinity', () => {
|
|
486
|
+
const normalTime = Time.fromMinutes(1000);
|
|
487
|
+
const infinity = Time.infinity();
|
|
488
|
+
|
|
489
|
+
assert.strictEqual(normalTime.isBefore(infinity), true);
|
|
490
|
+
assert.strictEqual(infinity.isAfter(normalTime), true);
|
|
491
|
+
assert.strictEqual(infinity.equals(normalTime), false);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
|
|
4
|
+
import { encodePickUpDropOffTypes } from '../../gtfs/trips.js';
|
|
4
5
|
import { Duration } from '../duration.js';
|
|
6
|
+
import { NOT_AVAILABLE, REGULAR, Route } from '../route.js';
|
|
5
7
|
import { Time } from '../time.js';
|
|
6
8
|
import {
|
|
7
9
|
RoutesAdjacency,
|
|
10
|
+
RouteType,
|
|
8
11
|
ServiceRoutesMap,
|
|
9
12
|
StopsAdjacency,
|
|
10
13
|
Timetable,
|
|
11
14
|
} from '../timetable.js';
|
|
12
15
|
|
|
13
|
-
describe('
|
|
16
|
+
describe('Timetable', () => {
|
|
14
17
|
const stopsAdjacency: StopsAdjacency = new Map([
|
|
15
18
|
[
|
|
16
19
|
1,
|
|
17
20
|
{
|
|
18
21
|
transfers: [{ destination: 2, type: 'RECOMMENDED' }],
|
|
19
|
-
routes: ['route1'],
|
|
22
|
+
routes: ['route1', 'route2'],
|
|
20
23
|
},
|
|
21
24
|
],
|
|
22
25
|
[
|
|
@@ -29,66 +32,51 @@ describe('timetable io', () => {
|
|
|
29
32
|
minTransferTime: Duration.fromMinutes(3),
|
|
30
33
|
},
|
|
31
34
|
],
|
|
32
|
-
routes: ['route2'],
|
|
35
|
+
routes: ['route2', 'route1'],
|
|
33
36
|
},
|
|
34
37
|
],
|
|
35
|
-
]);
|
|
36
|
-
const routesAdjacency: RoutesAdjacency = new Map([
|
|
37
38
|
[
|
|
38
|
-
|
|
39
|
+
3,
|
|
39
40
|
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Time.fromHMS(16, 50, 0).toMinutes(),
|
|
43
|
-
Time.fromHMS(17, 20, 0).toMinutes(),
|
|
44
|
-
Time.fromHMS(17, 30, 0).toMinutes(),
|
|
45
|
-
Time.fromHMS(18, 0, 0).toMinutes(),
|
|
46
|
-
Time.fromHMS(18, 10, 0).toMinutes(),
|
|
47
|
-
Time.fromHMS(19, 0, 0).toMinutes(),
|
|
48
|
-
Time.fromHMS(19, 10, 0).toMinutes(),
|
|
49
|
-
]),
|
|
50
|
-
pickUpDropOffTypes: new Uint8Array([
|
|
51
|
-
0,
|
|
52
|
-
0, // REGULAR
|
|
53
|
-
1,
|
|
54
|
-
0, // NOT_AVAILABLE, REGULAR
|
|
55
|
-
0,
|
|
56
|
-
0, // REGULAR
|
|
57
|
-
0,
|
|
58
|
-
0, // REGULAR
|
|
59
|
-
]),
|
|
60
|
-
stops: new Uint32Array([1, 2]),
|
|
61
|
-
stopIndices: new Map([
|
|
62
|
-
[1, 0],
|
|
63
|
-
[2, 1],
|
|
64
|
-
]),
|
|
65
|
-
serviceRouteId: 'gtfs1',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
[
|
|
69
|
-
'route2',
|
|
70
|
-
{
|
|
71
|
-
stopTimes: new Uint16Array([
|
|
72
|
-
Time.fromHMS(18, 20, 0).toMinutes(),
|
|
73
|
-
Time.fromHMS(18, 30, 0).toMinutes(),
|
|
74
|
-
Time.fromHMS(23, 20, 0).toMinutes(),
|
|
75
|
-
Time.fromHMS(23, 30, 0).toMinutes(),
|
|
76
|
-
]),
|
|
77
|
-
pickUpDropOffTypes: new Uint8Array([
|
|
78
|
-
0,
|
|
79
|
-
0, // REGULAR
|
|
80
|
-
0,
|
|
81
|
-
0, // REGULAR
|
|
82
|
-
]),
|
|
83
|
-
stops: new Uint32Array([2, 1]),
|
|
84
|
-
stopIndices: new Map([
|
|
85
|
-
[2, 0],
|
|
86
|
-
[1, 1],
|
|
87
|
-
]),
|
|
88
|
-
serviceRouteId: 'gtfs2',
|
|
41
|
+
transfers: [],
|
|
42
|
+
routes: [],
|
|
89
43
|
},
|
|
90
44
|
],
|
|
91
45
|
]);
|
|
46
|
+
|
|
47
|
+
const route1 = new Route(
|
|
48
|
+
new Uint16Array([
|
|
49
|
+
Time.fromHMS(16, 40, 0).toMinutes(),
|
|
50
|
+
Time.fromHMS(16, 50, 0).toMinutes(),
|
|
51
|
+
Time.fromHMS(17, 20, 0).toMinutes(),
|
|
52
|
+
Time.fromHMS(17, 30, 0).toMinutes(),
|
|
53
|
+
Time.fromHMS(18, 0, 0).toMinutes(),
|
|
54
|
+
Time.fromHMS(18, 10, 0).toMinutes(),
|
|
55
|
+
Time.fromHMS(19, 0, 0).toMinutes(),
|
|
56
|
+
Time.fromHMS(19, 10, 0).toMinutes(),
|
|
57
|
+
]),
|
|
58
|
+
encodePickUpDropOffTypes(
|
|
59
|
+
[REGULAR, NOT_AVAILABLE, REGULAR, REGULAR],
|
|
60
|
+
[REGULAR, REGULAR, REGULAR, REGULAR],
|
|
61
|
+
),
|
|
62
|
+
new Uint32Array([1, 2]),
|
|
63
|
+
'gtfs1',
|
|
64
|
+
);
|
|
65
|
+
const route2 = new Route(
|
|
66
|
+
new Uint16Array([
|
|
67
|
+
Time.fromHMS(18, 20, 0).toMinutes(),
|
|
68
|
+
Time.fromHMS(18, 30, 0).toMinutes(),
|
|
69
|
+
Time.fromHMS(23, 20, 0).toMinutes(),
|
|
70
|
+
Time.fromHMS(23, 30, 0).toMinutes(),
|
|
71
|
+
]),
|
|
72
|
+
encodePickUpDropOffTypes([REGULAR, REGULAR], [REGULAR, REGULAR]),
|
|
73
|
+
new Uint32Array([2, 1]),
|
|
74
|
+
'gtfs2',
|
|
75
|
+
);
|
|
76
|
+
const routesAdjacency: RoutesAdjacency = new Map([
|
|
77
|
+
['route1', route1],
|
|
78
|
+
['route2', route2],
|
|
79
|
+
]);
|
|
92
80
|
const routes: ServiceRoutesMap = new Map([
|
|
93
81
|
['gtfs1', { type: 'RAIL', name: 'Route 1' }],
|
|
94
82
|
['gtfs2', { type: 'RAIL', name: 'Route 2' }],
|
|
@@ -114,7 +102,7 @@ describe('timetable io', () => {
|
|
|
114
102
|
it('should find the earliest trip for stop1 on route1', () => {
|
|
115
103
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
116
104
|
const route = sampleTimetable.getRoute('route1')!;
|
|
117
|
-
const tripIndex =
|
|
105
|
+
const tripIndex = route.findEarliestTrip(1);
|
|
118
106
|
assert.strictEqual(tripIndex, 0);
|
|
119
107
|
});
|
|
120
108
|
|
|
@@ -122,12 +110,7 @@ describe('timetable io', () => {
|
|
|
122
110
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
123
111
|
const route = sampleTimetable.getRoute('route1')!;
|
|
124
112
|
const afterTime = Time.fromHMS(17, 0, 0);
|
|
125
|
-
const tripIndex =
|
|
126
|
-
route,
|
|
127
|
-
1,
|
|
128
|
-
undefined,
|
|
129
|
-
afterTime,
|
|
130
|
-
);
|
|
113
|
+
const tripIndex = route.findEarliestTrip(1, afterTime);
|
|
131
114
|
assert.strictEqual(tripIndex, 1);
|
|
132
115
|
});
|
|
133
116
|
|
|
@@ -135,38 +118,40 @@ describe('timetable io', () => {
|
|
|
135
118
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
136
119
|
const route = sampleTimetable.getRoute('route1')!;
|
|
137
120
|
const afterTime = Time.fromHMS(23, 40, 0);
|
|
138
|
-
const tripIndex =
|
|
139
|
-
route,
|
|
140
|
-
1,
|
|
141
|
-
undefined,
|
|
142
|
-
afterTime,
|
|
143
|
-
);
|
|
121
|
+
const tripIndex = route.findEarliestTrip(1, afterTime);
|
|
144
122
|
assert.strictEqual(tripIndex, undefined);
|
|
145
123
|
});
|
|
146
124
|
it('should return undefined if the stop on a trip has pick up not available', () => {
|
|
147
125
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
148
126
|
const route = sampleTimetable.getRoute('route1')!;
|
|
149
|
-
const tripIndex =
|
|
127
|
+
const tripIndex = route.findEarliestTrip(2);
|
|
150
128
|
assert.strictEqual(tripIndex, 1);
|
|
151
129
|
});
|
|
152
130
|
it('should find reachable routes from a set of stop IDs', () => {
|
|
153
131
|
const fromStops = new Set([1]);
|
|
154
132
|
const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
|
|
155
|
-
assert.strictEqual(reachableRoutes.size,
|
|
156
|
-
assert.
|
|
133
|
+
assert.strictEqual(reachableRoutes.size, 2);
|
|
134
|
+
assert.deepStrictEqual(
|
|
135
|
+
reachableRoutes,
|
|
136
|
+
new Map([
|
|
137
|
+
[route1, 1],
|
|
138
|
+
[route2, 1],
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
157
141
|
});
|
|
158
142
|
|
|
159
143
|
it('should find no reachable routes if starting from a non-existent stop', () => {
|
|
160
|
-
const fromStops = new Set([
|
|
144
|
+
const fromStops = new Set([3]);
|
|
161
145
|
const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
|
|
162
146
|
assert.strictEqual(reachableRoutes.size, 0);
|
|
163
147
|
});
|
|
164
148
|
|
|
165
149
|
it('should find reachable routes filtered by transport modes', () => {
|
|
166
150
|
const fromStops = new Set([1]);
|
|
167
|
-
const reachableRoutes = sampleTimetable.findReachableRoutes(
|
|
168
|
-
|
|
169
|
-
|
|
151
|
+
const reachableRoutes = sampleTimetable.findReachableRoutes(
|
|
152
|
+
fromStops,
|
|
153
|
+
new Set<RouteType>(['BUS']),
|
|
154
|
+
);
|
|
170
155
|
assert.strictEqual(reachableRoutes.size, 0);
|
|
171
156
|
});
|
|
172
157
|
});
|