minotor 3.0.1 → 3.0.2
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 +2 -2
- package/README.md +34 -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 +1967 -823
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +1 -0
- package/dist/gtfs/utils.d.ts +1 -1
- package/dist/parser.cjs.js +1030 -627
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.d.ts +4 -2
- package/dist/parser.esm.js +1030 -627
- 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 +1 -1
- package/dist/timetable/route.d.ts +155 -0
- package/dist/timetable/time.d.ts +21 -0
- package/dist/timetable/timetable.d.ts +41 -61
- 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 +26 -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 +67 -74
- 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 +33 -16
- 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 +317 -0
- package/src/timetable/__tests__/time.test.ts +494 -0
- package/src/timetable/__tests__/timetable.test.ts +64 -75
- package/src/timetable/io.ts +32 -26
- package/src/timetable/proto/timetable.proto +1 -1
- package/src/timetable/proto/timetable.ts +13 -13
- package/src/timetable/route.ts +347 -0
- package/src/timetable/time.ts +40 -8
- package/src/timetable/timetable.ts +74 -165
- 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
|
+
});
|
|
@@ -2,21 +2,23 @@ import assert from 'node:assert';
|
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
|
|
4
4
|
import { Duration } from '../duration.js';
|
|
5
|
+
import { NOT_AVAILABLE, REGULAR, Route } from '../route.js';
|
|
5
6
|
import { Time } from '../time.js';
|
|
6
7
|
import {
|
|
7
8
|
RoutesAdjacency,
|
|
9
|
+
RouteType,
|
|
8
10
|
ServiceRoutesMap,
|
|
9
11
|
StopsAdjacency,
|
|
10
12
|
Timetable,
|
|
11
13
|
} from '../timetable.js';
|
|
12
14
|
|
|
13
|
-
describe('
|
|
15
|
+
describe('Timetable', () => {
|
|
14
16
|
const stopsAdjacency: StopsAdjacency = new Map([
|
|
15
17
|
[
|
|
16
18
|
1,
|
|
17
19
|
{
|
|
18
20
|
transfers: [{ destination: 2, type: 'RECOMMENDED' }],
|
|
19
|
-
routes: ['route1'],
|
|
21
|
+
routes: ['route1', 'route2'],
|
|
20
22
|
},
|
|
21
23
|
],
|
|
22
24
|
[
|
|
@@ -29,66 +31,56 @@ describe('timetable io', () => {
|
|
|
29
31
|
minTransferTime: Duration.fromMinutes(3),
|
|
30
32
|
},
|
|
31
33
|
],
|
|
32
|
-
routes: ['route2'],
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
|
-
]);
|
|
36
|
-
const routesAdjacency: RoutesAdjacency = new Map([
|
|
37
|
-
[
|
|
38
|
-
'route1',
|
|
39
|
-
{
|
|
40
|
-
stopTimes: new Uint16Array([
|
|
41
|
-
Time.fromHMS(16, 40, 0).toMinutes(),
|
|
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',
|
|
34
|
+
routes: ['route2', 'route1'],
|
|
66
35
|
},
|
|
67
36
|
],
|
|
68
37
|
[
|
|
69
|
-
|
|
38
|
+
3,
|
|
70
39
|
{
|
|
71
|
-
|
|
72
|
-
|
|
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',
|
|
40
|
+
transfers: [],
|
|
41
|
+
routes: [],
|
|
89
42
|
},
|
|
90
43
|
],
|
|
91
44
|
]);
|
|
45
|
+
const route1 = new Route(
|
|
46
|
+
new Uint16Array([
|
|
47
|
+
Time.fromHMS(16, 40, 0).toMinutes(),
|
|
48
|
+
Time.fromHMS(16, 50, 0).toMinutes(),
|
|
49
|
+
Time.fromHMS(17, 20, 0).toMinutes(),
|
|
50
|
+
Time.fromHMS(17, 30, 0).toMinutes(),
|
|
51
|
+
Time.fromHMS(18, 0, 0).toMinutes(),
|
|
52
|
+
Time.fromHMS(18, 10, 0).toMinutes(),
|
|
53
|
+
Time.fromHMS(19, 0, 0).toMinutes(),
|
|
54
|
+
Time.fromHMS(19, 10, 0).toMinutes(),
|
|
55
|
+
]),
|
|
56
|
+
new Uint8Array([
|
|
57
|
+
REGULAR,
|
|
58
|
+
REGULAR,
|
|
59
|
+
NOT_AVAILABLE,
|
|
60
|
+
REGULAR,
|
|
61
|
+
REGULAR,
|
|
62
|
+
REGULAR,
|
|
63
|
+
REGULAR,
|
|
64
|
+
REGULAR,
|
|
65
|
+
]),
|
|
66
|
+
new Uint32Array([1, 2]),
|
|
67
|
+
'gtfs1',
|
|
68
|
+
);
|
|
69
|
+
const route2 = new Route(
|
|
70
|
+
new Uint16Array([
|
|
71
|
+
Time.fromHMS(18, 20, 0).toMinutes(),
|
|
72
|
+
Time.fromHMS(18, 30, 0).toMinutes(),
|
|
73
|
+
Time.fromHMS(23, 20, 0).toMinutes(),
|
|
74
|
+
Time.fromHMS(23, 30, 0).toMinutes(),
|
|
75
|
+
]),
|
|
76
|
+
new Uint8Array([REGULAR, REGULAR, REGULAR, REGULAR]),
|
|
77
|
+
new Uint32Array([2, 1]),
|
|
78
|
+
'gtfs2',
|
|
79
|
+
);
|
|
80
|
+
const routesAdjacency: RoutesAdjacency = new Map([
|
|
81
|
+
['route1', route1],
|
|
82
|
+
['route2', route2],
|
|
83
|
+
]);
|
|
92
84
|
const routes: ServiceRoutesMap = new Map([
|
|
93
85
|
['gtfs1', { type: 'RAIL', name: 'Route 1' }],
|
|
94
86
|
['gtfs2', { type: 'RAIL', name: 'Route 2' }],
|
|
@@ -114,7 +106,7 @@ describe('timetable io', () => {
|
|
|
114
106
|
it('should find the earliest trip for stop1 on route1', () => {
|
|
115
107
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
116
108
|
const route = sampleTimetable.getRoute('route1')!;
|
|
117
|
-
const tripIndex =
|
|
109
|
+
const tripIndex = route.findEarliestTrip(1);
|
|
118
110
|
assert.strictEqual(tripIndex, 0);
|
|
119
111
|
});
|
|
120
112
|
|
|
@@ -122,12 +114,7 @@ describe('timetable io', () => {
|
|
|
122
114
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
123
115
|
const route = sampleTimetable.getRoute('route1')!;
|
|
124
116
|
const afterTime = Time.fromHMS(17, 0, 0);
|
|
125
|
-
const tripIndex =
|
|
126
|
-
route,
|
|
127
|
-
1,
|
|
128
|
-
undefined,
|
|
129
|
-
afterTime,
|
|
130
|
-
);
|
|
117
|
+
const tripIndex = route.findEarliestTrip(1, afterTime);
|
|
131
118
|
assert.strictEqual(tripIndex, 1);
|
|
132
119
|
});
|
|
133
120
|
|
|
@@ -135,38 +122,40 @@ describe('timetable io', () => {
|
|
|
135
122
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
136
123
|
const route = sampleTimetable.getRoute('route1')!;
|
|
137
124
|
const afterTime = Time.fromHMS(23, 40, 0);
|
|
138
|
-
const tripIndex =
|
|
139
|
-
route,
|
|
140
|
-
1,
|
|
141
|
-
undefined,
|
|
142
|
-
afterTime,
|
|
143
|
-
);
|
|
125
|
+
const tripIndex = route.findEarliestTrip(1, afterTime);
|
|
144
126
|
assert.strictEqual(tripIndex, undefined);
|
|
145
127
|
});
|
|
146
128
|
it('should return undefined if the stop on a trip has pick up not available', () => {
|
|
147
129
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
148
130
|
const route = sampleTimetable.getRoute('route1')!;
|
|
149
|
-
const tripIndex =
|
|
131
|
+
const tripIndex = route.findEarliestTrip(2);
|
|
150
132
|
assert.strictEqual(tripIndex, 1);
|
|
151
133
|
});
|
|
152
134
|
it('should find reachable routes from a set of stop IDs', () => {
|
|
153
135
|
const fromStops = new Set([1]);
|
|
154
136
|
const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
|
|
155
|
-
assert.strictEqual(reachableRoutes.size,
|
|
156
|
-
assert.
|
|
137
|
+
assert.strictEqual(reachableRoutes.size, 2);
|
|
138
|
+
assert.deepStrictEqual(
|
|
139
|
+
reachableRoutes,
|
|
140
|
+
new Map([
|
|
141
|
+
[route1, 1],
|
|
142
|
+
[route2, 1],
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
157
145
|
});
|
|
158
146
|
|
|
159
147
|
it('should find no reachable routes if starting from a non-existent stop', () => {
|
|
160
|
-
const fromStops = new Set([
|
|
148
|
+
const fromStops = new Set([3]);
|
|
161
149
|
const reachableRoutes = sampleTimetable.findReachableRoutes(fromStops);
|
|
162
150
|
assert.strictEqual(reachableRoutes.size, 0);
|
|
163
151
|
});
|
|
164
152
|
|
|
165
153
|
it('should find reachable routes filtered by transport modes', () => {
|
|
166
154
|
const fromStops = new Set([1]);
|
|
167
|
-
const reachableRoutes = sampleTimetable.findReachableRoutes(
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
const reachableRoutes = sampleTimetable.findReachableRoutes(
|
|
156
|
+
fromStops,
|
|
157
|
+
new Set<RouteType>(['BUS']),
|
|
158
|
+
);
|
|
170
159
|
assert.strictEqual(reachableRoutes.size, 0);
|
|
171
160
|
});
|
|
172
161
|
});
|