• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OneBusAway / wayfinder / 23265247142

18 Mar 2026 08:22PM UTC coverage: 80.013% (-0.03%) from 80.047%
23265247142

push

github

web-flow
Merge pull request #394 from OneBusAway/2026.5

2026.5

1789 of 1981 branches covered (90.31%)

Branch coverage included in aggregate %.

155 of 253 new or added lines in 13 files covered. (61.26%)

5 existing lines in 5 files now uncovered.

11326 of 14410 relevant lines covered (78.6%)

4.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.41
/src/lib/dateTimeFormat.js
1
export function getLocalTimeZone() {
1✔
2
        return new Intl.DateTimeFormat().resolvedOptions().timeZone;
77✔
3
}
77✔
4

5
// Time formats
1✔
6
export const utcTimeFormat = new Intl.DateTimeFormat(undefined, {
1✔
7
        hour: 'numeric',
1✔
8
        minute: '2-digit',
1✔
9
        hour12: true,
1✔
10
        timeZone: 'UTC'
1✔
11
});
1✔
12

13
export const localTimeFormat = new Intl.DateTimeFormat(undefined, {
1✔
14
        hour: 'numeric',
1✔
15
        minute: '2-digit',
1✔
16
        hour12: true
1✔
17
});
1✔
18

19
export const fourDigitTimeFormat = new Intl.DateTimeFormat(undefined, {
1✔
20
        hour: '2-digit',
1✔
21
        minute: '2-digit',
1✔
22
        hour12: true
1✔
23
});
1✔
24

25
// Date formats
1✔
26
export const apiDateFormat = new Intl.DateTimeFormat('en-US', {
1✔
27
        year: 'numeric',
1✔
28
        month: '2-digit',
1✔
29
        day: '2-digit'
1✔
30
});
1✔
31

32
export const apiTimeFormat = new Intl.DateTimeFormat('en-US', {
1✔
33
        hour: 'numeric',
1✔
34
        minute: '2-digit',
1✔
35
        hour12: true
1✔
36
});
1✔
37

38
/**
1✔
39
 * Format milliseconds since Unix epoch to a given time zone and format
1✔
40
 *
1✔
41
 * @example
1✔
42
 * msToTimeString(1705395900000)  // Returns '1:05 AM' assuming the local timezone is America/Los_Angeles
1✔
43
 * msToTimeString(1705395900000, 'UTC')  // Returns '9:05 AM'
1✔
44
 * msToTimeString(1705395900000, 'America/New_York')  // Returns '4:05 AM'
1✔
45
 * msToTimeString(1705395900000, 'America/New_York', fourDigitTimeFormat)  // Returns '04:05 AM'
1✔
46
 *
1✔
47
 * @param {number} ms - Milliseconds since Unix epoch
1✔
48
 * @param {string} [timeZone] - IANA timezone. Defaults to the local timezone.
1✔
49
 * @param {Intl.DateTimeFormat} [dateTimeFormat] - Intl.DateTimeFormat to use for formatting. Defaults to localTimeFormat.
1✔
50
 * @returns {string} Time in the given format
1✔
51
 */
1✔
52
export function msToTimeString(
1✔
53
        ms,
26✔
54
        timeZone = getLocalTimeZone(),
26✔
55
        dateTimeFormat = localTimeFormat
26✔
56
) {
26✔
57
        if (!Number.isFinite(ms)) return 'N/A';
26✔
58
        const instant = Temporal.Instant.fromEpochMilliseconds(ms);
14✔
59
        try {
14✔
60
                const plainTime = instant.toZonedDateTimeISO(timeZone).toPlainTime();
14✔
61
                return dateTimeFormat.format(plainTime);
14✔
62
        } catch (err) {
26✔
63
                if (err instanceof RangeError) {
1✔
64
                        console.error(`msToTimeString: invalid timezone "${timeZone}", falling back to local`);
1✔
65
                        const plainTime = instant.toZonedDateTimeISO(getLocalTimeZone()).toPlainTime();
1✔
66
                        return dateTimeFormat.format(plainTime);
1✔
67
                }
1!
NEW
68
                throw err;
×
NEW
69
        }
×
70
}
26✔
71

72
/**
1✔
73
 * Format milliseconds since Unix epoch to "HH:mm AM/PM" format
1✔
74
 * Dates are in local timezone
1✔
75
 *
1✔
76
 * @example
1✔
77
 * (Assuming the local timezone is America/Los_Angeles)
1✔
78
 * msToLocalArrivalDepartureTimeString(1705425300000)  // Returns '09:15 AM'
1✔
79
 *
1✔
80
 * @param {number} ms - Milliseconds since Unix epoch
1✔
81
 * @returns {string} Time in "HH:mm AM/PM" format
1✔
82
 */
1✔
83
export function msToLocalArrivalDepartureTimeString(ms) {
1✔
84
        return msToTimeString(ms, getLocalTimeZone(), fourDigitTimeFormat);
11✔
85
}
11✔
86

87
/**
1✔
88
 * Show the time in "h:mm AM/PM" format for a given number of seconds since midnight.
1✔
89
 *
1✔
90
 * @param {number} secondsSinceMidnight - Number of seconds since midnight
1✔
91
 * @returns {string} Time in "h:mm AM/PM" format
1✔
92
 *
1✔
93
 * @example
1✔
94
 * formatSecondsFromMidnight(38280)  // Returns '10:38 AM'
1✔
95
 */
1✔
96
export function formatSecondsFromMidnight(secondsSinceMidnight) {
1✔
97
        if (!Number.isFinite(secondsSinceMidnight)) return '';
13✔
98

99
        const midnight = new Temporal.PlainTime();
7✔
100
        const time = midnight.add({ seconds: secondsSinceMidnight });
7✔
101

102
        return utcTimeFormat.format(time);
7✔
103
}
7✔
104

105
/**
1✔
106
 * Helper to format departure time for pill display
1✔
107
 * Accepts an optional translator function for i18n support
1✔
108
 * @param {Object} opts - Options object containing departureType, departureTime, and departureDate
1✔
109
 * @param {string} [opts.departureType] - Departure type ('departAt' | 'arriveBy' | 'now')
1✔
110
 * @param {string} [opts.departureTime] - Departure time in 'HH:mm' format
1✔
111
 * @param {string} [opts.departureDate] - Departure date in 'YYYY-MM-DD' format
1✔
112
 * @param {Function} [translator] - Optional translator function for i18n support
1✔
113
 * @returns {string|null} Formatted departure time string, or null if departureType is 'now'
1✔
114
 *
1✔
115
 * @example
1✔
116
 * formatDepartureDisplay({ departureType: 'departAt', departureTime: '09:00', departureDate: null })  // Returns 'Depart 9:00 AM'
1✔
117
 * formatDepartureDisplay({ departureType: 'arriveBy', departureTime: '17:00', departureDate: '2025-06-15' }, translator)  // Returns 'Arrive 5:00 PM, Today' (assuming today is 2025-06-15)
1✔
118
 */
1✔
119
export function formatDepartureDisplay(opts, translator = null) {
1✔
120
        if (opts.departureType === 'now') return null;
12✔
121

122
        const timeStr = opts.departureTime || '';
12✔
123
        const dateStr = opts.departureDate || '';
12✔
124

125
        // Use translator if provided, otherwise fall back to English
12✔
126
        const prefix =
12✔
127
                opts.departureType === 'arriveBy'
12✔
128
                        ? translator
12✔
129
                                ? translator('trip-planner.arrive')
3✔
130
                                : 'Arrive'
3✔
131
                        : translator
12✔
132
                                ? translator('trip-planner.depart')
8✔
133
                                : 'Depart';
12✔
134

135
        if (timeStr) {
12✔
136
                const formattedTime = parseTimeInput(timeStr);
9✔
137
                if (!formattedTime) return prefix;
9✔
138

139
                let dateSuffix = '';
8✔
140
                if (dateStr) {
9✔
141
                        const today = Temporal.Now.plainDateISO();
6✔
142
                        const tomorrow = today.add({ days: 1 });
6✔
143

144
                        if (dateStr === today.toJSON()) {
6✔
145
                                const todayLabel = translator ? translator('trip-planner.today') : 'Today';
2!
146
                                dateSuffix = `, ${todayLabel}`;
2✔
147
                        } else if (dateStr === tomorrow.toJSON()) {
6✔
148
                                const tomorrowLabel = translator ? translator('trip-planner.tomorrow') : 'Tomorrow';
2✔
149
                                dateSuffix = `, ${tomorrowLabel}`;
2✔
150
                        } else {
2✔
151
                                dateSuffix = `, ${dateStr}`;
2✔
152
                        }
2✔
153
                }
6✔
154

155
                return `${prefix} ${formattedTime}${dateSuffix}`;
8✔
156
        }
8✔
157

158
        return prefix;
2✔
159
}
2✔
160

161
/**
1✔
162
 * Parse HTML time input (HH:mm, 24-hour) to OTP format (h:mm AM/PM).
1✔
163
 *
1✔
164
 * Uses Temporal.PlainTime.from to parse the time string to avoid timezone issues.
1✔
165
 * If the time string is already in "h:mm AM/PM" format, it is returned unchanged.
1✔
166
 *
1✔
167
 * @param {string} timeString - Time in "HH:mm" format (24-hour)
1✔
168
 * @returns {string|null} Time in "h:mm AM/PM" format, or null if invalid
1✔
169
 *
1✔
170
 * @example
1✔
171
 * parseTimeInput('14:30')  // Returns '2:30 PM'
1✔
172
 * parseTimeInput('00:00')  // Returns '12:00 AM'
1✔
173
 * parseTimeInput('12:00')  // Returns '12:00 PM'
1✔
174
 * parseTimeInput('09:05')  // Returns '9:05 AM'
1✔
175
 */
1✔
176
export function parseTimeInput(timeString) {
1✔
177
        if (!timeString || typeof timeString !== 'string') {
45✔
178
                return null;
5✔
179
        }
5✔
180

181
        // Try to parse as already-converted format
40✔
182
        const matchAlreadyConverted = timeString.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
40✔
183
        if (matchAlreadyConverted) {
45✔
184
                return timeString;
2✔
185
        }
2✔
186

187
        // Try to parse as 24-hour format
38✔
188
        const match24Hour = timeString.match(/^(\d{2}):(\d{2})$/);
38✔
189
        if (!match24Hour) {
45✔
190
                return null;
7✔
191
        }
7✔
192

193
        try {
31✔
194
                const time = Temporal.PlainTime.from(timeString);
31✔
195
                return apiTimeFormat.format(time);
31✔
196
        } catch (err) {
45✔
197
                if (err instanceof RangeError) return null;
4✔
198
                throw err;
1✔
199
        }
1✔
200
}
45✔
201

202
/**
1✔
203
 * Parse HTML date input (YYYY-MM-DD) to OTP format (MM-DD-YYYY).
1✔
204
 *
1✔
205
 * Uses Temporal.PlainDate.from() for validation and parsing.
1✔
206
 *
1✔
207
 * @param {string} dateString - Date in "YYYY-MM-DD" format
1✔
208
 * @returns {string|null} Date in "MM-DD-YYYY" format, or null if invalid
1✔
209
 *
1✔
210
 * @example
1✔
211
 * parseDateInput('2026-01-14')  // Returns '01-14-2026'
1✔
212
 * parseDateInput('2026-12-31')  // Returns '12-31-2026'
1✔
213
 */
1✔
214
export function parseDateInput(dateString) {
1✔
215
        if (!dateString || typeof dateString !== 'string') {
29✔
216
                return null;
5✔
217
        }
5✔
218

219
        const match = dateString.match(/^(\d{4})-(\d{2})-(\d{2})$/);
24✔
220
        if (!match) {
29✔
221
                return null;
4✔
222
        }
4✔
223

224
        try {
20✔
225
                const dateTime = Temporal.PlainDate.from(dateString);
20✔
226
                if (dateTime.year < 2000 || dateTime.year > 2100) {
29✔
227
                        return null;
2✔
228
                }
2✔
229
                return apiDateFormat.format(dateTime).replaceAll('/', '-');
13✔
230
        } catch (err) {
14✔
231
                if (err instanceof RangeError) return null;
5✔
232
                throw err;
1✔
233
        }
1✔
234
}
29✔
235

236
/**
1✔
237
 * Format a 24-hour hour to 12-hour format
1✔
238
 *
1✔
239
 * @param {number} hour - Hour in 24-hour format
1✔
240
 * @returns {number|null} Hour in 12-hour format, or null if invalid
1✔
241
 *
1✔
242
 * @example
1✔
243
 * convert24HourTo12Hour(0)  // Returns 12
1✔
244
 * convert24HourTo12Hour(12)  // Returns 12
1✔
245
 * convert24HourTo12Hour(14)  // Returns 2
1✔
246
 * convert24HourTo12Hour(23)  // Returns 11
1✔
247
 */
1✔
248
export function convert24HourTo12Hour(hour) {
1✔
249
        const hourNum = typeof hour === 'string' ? Number(hour) : hour;
14✔
250
        if (!Number.isFinite(hourNum)) return null;
14✔
251
        if (hourNum < 0 || hourNum > 23) return null;
14✔
252
        if (hourNum === 0) return 12;
14✔
253
        if (hourNum > 12) return hourNum - 12;
14✔
254
        return hourNum;
2✔
255
}
2✔
256

257
/**
1✔
258
 * Format a Date object to OTP API time format: "h:mm AM/PM"
1✔
259
 *
1✔
260
 * @param {Date} date - Date object
1✔
261
 * @param {string} [timeZone] - IANA timezone (e.g. "America/Los_Angeles"). Defaults to local.
1✔
262
 * @returns {string} Time in "h:mm AM/PM" format
1✔
263
 *
1✔
264
 * @example
1✔
265
 * formatTimeForOTP(new Date('2026-01-14T14:30:00'))  // Returns '2:30 PM'
1✔
266
 */
1✔
267
export function formatTimeForOTP(date, timeZone) {
1✔
268
        if (timeZone) {
11✔
269
                return new Intl.DateTimeFormat('en-US', {
5✔
270
                        hour: 'numeric',
5✔
271
                        minute: '2-digit',
5✔
272
                        hour12: true,
5✔
273
                        timeZone
5✔
274
                }).format(date);
5✔
275
        }
5✔
276
        return apiTimeFormat.format(date);
6✔
277
}
6✔
278

279
/**
1✔
280
 * Format a Date object to OTP API date format: "MM-DD-YYYY"
1✔
281
 * Used for "Leave Now" mode where we need the current date.
1✔
282
 *
1✔
283
 * @param {Date} date - Date object
1✔
284
 * @param {string} [timeZone] - IANA timezone (e.g. "America/Los_Angeles"). Defaults to local.
1✔
285
 * @returns {string} Date in "MM-DD-YYYY" format
1✔
286
 *
1✔
287
 * @example
1✔
288
 * formatDateForOTP(new Date(2026, 0, 14))  // Returns '01-14-2026'
1✔
289
 */
1✔
290
export function formatDateForOTP(date, timeZone) {
1✔
291
        if (timeZone) {
10✔
292
                return new Intl.DateTimeFormat('en-US', {
5✔
293
                        year: 'numeric',
5✔
294
                        month: '2-digit',
5✔
295
                        day: '2-digit',
5✔
296
                        timeZone
5✔
297
                })
5✔
298
                        .format(date)
5✔
299
                        .replaceAll('/', '-');
5✔
300
        }
5✔
301
        return apiDateFormat.format(date).replaceAll('/', '-');
5✔
302
}
5✔
303

304
/**
1✔
305
 * Format a timestamp to a last updated string
1✔
306
 *
1✔
307
 * @param {number} timestamp - Timestamp in milliseconds since Unix epoch
1✔
308
 * @param {Object} translations - Object containing translation strings for minutes, seconds, and ago
1✔
309
 * @param {string} translations.min - Translation string for minutes
1✔
310
 * @param {string} translations.sec - Translation string for seconds
1✔
311
 * @param {string} translations.ago - Translation string for ago
1✔
312
 * @returns {string} Formatted last updated string
1✔
313
 *
1✔
314
 * @example
1✔
315
 * Note: The actual output of these examples depends on the current time
1✔
316
 * formatLastUpdated(1715894400000, { min: 'min', sec: 'sec', ago: 'ago' })  // Returns '1 min 30 sec ago'
1✔
317
 * formatLastUpdated(1715894400000, { min: 'minute', sec: 'second', ago: 'ago' })  // Returns '1 minute 30 second ago'
1✔
318
 */
1✔
319
export function formatLastUpdated(timestamp, translations) {
1✔
320
        if (!Number.isFinite(timestamp)) return 'N/A';
12✔
321
        const date = Temporal.Instant.fromEpochMilliseconds(timestamp);
6✔
322
        const now = Temporal.Now.instant();
6✔
323
        const { minutes, seconds } = now.since(date).round({ largestUnit: 'minute' });
6✔
324

325
        const minutesStr = minutes > 0 ? `${minutes} ${translations.min} ` : '';
12✔
326
        return `${minutesStr}${seconds} ${translations.sec} ${translations.ago}`;
12✔
327
}
12✔
328

329
/**
1✔
330
 * Convert date ("MM-DD-YYYY") + time ("h:mm AM/PM") to OffsetDateTime
1✔
331
 * ("YYYY-MM-DDThh:mm:ss±HH:MM") as required by OTP 2.x GraphQL API.
1✔
332
 *
1✔
333
 * The timezone is used to compute the correct UTC offset for the target date,
1✔
334
 * handling DST transitions correctly. When timeZone is omitted, falls back to
1✔
335
 * the server process's locale.
1✔
336
 *
1✔
337
 * @param {string} date - Date in "MM-DD-YYYY" format
1✔
338
 * @param {string} time - Time in "h:mm AM/PM" format
1✔
339
 * @param {string} [timeZone] - IANA timezone (e.g. "America/Los_Angeles"). Defaults to server locale.
1✔
340
 * @returns {string|null} OffsetDateTime string, or null if time or date format is invalid
1✔
341
 */
1✔
342
export function convertToISO8601(date, time, timeZone) {
1✔
343
        if (!date || typeof date !== 'string') return null;
54✔
344
        if (!time || typeof time !== 'string') return null;
54✔
345

346
        const dateMatch = date.match(/^(\d{2})-(\d{2})-(\d{4})$/);
50✔
347
        if (!dateMatch) return null;
54✔
348

349
        const timeMatch = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
49✔
350
        if (!timeMatch) return null;
54✔
351

352
        const month = parseInt(dateMatch[1], 10);
46✔
353
        const day = parseInt(dateMatch[2], 10);
46✔
354
        const year = parseInt(dateMatch[3], 10);
46✔
355

356
        let hour = parseInt(timeMatch[1], 10);
46✔
357
        const minute = parseInt(timeMatch[2], 10);
46✔
358
        const period = timeMatch[3].toUpperCase();
46✔
359

360
        if (period === 'AM' && hour === 12) hour = 0;
54✔
361
        else if (period === 'PM' && hour !== 12) hour += 12;
43✔
362

363
        try {
46✔
364
                const plainDateTime = Temporal.PlainDateTime.from({ year, month, day, hour, minute });
46✔
365
                // toZonedDateTime resolves DST correctly for the target date
46✔
366
                const zdt = plainDateTime.toZonedDateTime(timeZone || getLocalTimeZone());
54✔
367

368
                // Use Temporal-resolved values so DST gaps produce a valid datetime
54✔
369
                // (e.g. 2:30 AM during spring-forward resolves to 3:30 AM)
54✔
370
                const yearStr = String(zdt.year).padStart(4, '0');
54✔
371
                const monthStr = String(zdt.month).padStart(2, '0');
54✔
372
                const dayStr = String(zdt.day).padStart(2, '0');
54✔
373
                const hourStr = String(zdt.hour).padStart(2, '0');
54✔
374
                const minuteStr = String(zdt.minute).padStart(2, '0');
54✔
375

376
                return `${yearStr}-${monthStr}-${dayStr}T${hourStr}:${minuteStr}:00${zdt.offset}`;
54✔
377
        } catch (err) {
54✔
378
                if (err instanceof RangeError) return null;
1!
NEW
379
                throw err;
×
UNCOV
380
        }
×
381
}
54✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc