@prairielearn/formatter 1.4.1 → 1.4.3
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/CHANGELOG.md +12 -0
- package/dist/date.d.ts +8 -2
- package/dist/date.js +55 -14
- package/dist/date.js.map +1 -1
- package/dist/date.test.js +281 -78
- package/dist/date.test.js.map +1 -1
- package/package.json +2 -2
- package/src/date.test.ts +408 -121
- package/src/date.ts +74 -10
package/src/date.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { toTemporalInstant } from '@js-temporal/polyfill';
|
|
2
2
|
import keyBy from 'lodash/keyBy.js';
|
|
3
3
|
|
|
4
|
+
type TimePrecision = 'hour' | 'minute' | 'second';
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Format a date to a human-readable string like '2020-03-27T12:34:56 (CDT)'.
|
|
6
8
|
*
|
|
@@ -214,14 +216,21 @@ export function formatDateWithinRange(
|
|
|
214
216
|
* - '3:34pm'
|
|
215
217
|
* - '3:34:17pm'
|
|
216
218
|
*
|
|
219
|
+
* maxPrecision must be an equal or smaller unit than minPrecision.
|
|
220
|
+
*
|
|
217
221
|
* @param date The date to format.
|
|
218
222
|
* @param timezone The time zone to use for formatting.
|
|
219
223
|
* @param baseDate The base date to use for comparison.
|
|
224
|
+
* @param maxPrecision Only show units as large or larger than the max precision.
|
|
225
|
+
* @param minPrecision Always show that unit and larger, potentially showing smaller units.
|
|
226
|
+
*
|
|
220
227
|
*/
|
|
221
228
|
function formatDateFriendlyParts(
|
|
222
229
|
date: Date,
|
|
223
230
|
timezone: string,
|
|
224
231
|
baseDate: Date,
|
|
232
|
+
maxPrecision: TimePrecision = 'second',
|
|
233
|
+
minPrecision: TimePrecision = 'hour',
|
|
225
234
|
): { dateFormatted: string; timeFormatted: string; timezoneFormatted: string } {
|
|
226
235
|
// compute the number of days from the base date (0 = today, 1 = tomorrow, etc.)
|
|
227
236
|
|
|
@@ -265,15 +274,60 @@ function formatDateFriendlyParts(
|
|
|
265
274
|
dateFormatted = `${parts.weekday.value}, ${parts.month.value}\u00a0${parts.day.value}, ${parts.year.value}`;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
|
|
277
|
+
const precisionOrder: TimePrecision[] = ['second', 'minute', 'hour'];
|
|
278
|
+
const maxIndex = precisionOrder.indexOf(maxPrecision);
|
|
279
|
+
const minIndex = precisionOrder.indexOf(minPrecision);
|
|
269
280
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
/**
|
|
282
|
+
* The maximum precision must be a unit smaller than or equal to the minimum precision, otherwise the rules will contradict each other.
|
|
283
|
+
*
|
|
284
|
+
* If max is a larger unit than min, e.g. max = hour, min = minute, then by "min"
|
|
285
|
+
* we must display minute and smaller but by "max" we can display hour and larger, which is a contradiction.
|
|
286
|
+
*
|
|
287
|
+
* If min is a larger unit than max, e.g. max = minute, min = hour, then by "min" we must display
|
|
288
|
+
* hour and smaller and by "max" we can display minutes and larger. These do not contradict each other.
|
|
289
|
+
*
|
|
290
|
+
* V min/max > | h | m | s
|
|
291
|
+
* h | X | X | X
|
|
292
|
+
* m | I | X | X
|
|
293
|
+
* s | I | I | X
|
|
294
|
+
*
|
|
295
|
+
* X - valid configuration
|
|
296
|
+
* I - invalid configuration
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
// A higher index corresponds to a larger unit, so if maxIndex is larger than minIndex, then the rules contradict each other.
|
|
300
|
+
if (maxIndex > minIndex) {
|
|
301
|
+
throw new Error('maxPrecision must be an equal or smaller unit than minPrecision.');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Examples:
|
|
305
|
+
* min=h, max=h: 0:00:00AM -> 0AM, 0:00:01AM -> 0AM, 0:01:01AM -> 0AM
|
|
306
|
+
* min=h, max=m: 0:00:00AM -> 0AM, 0:00:01AM -> 0AM, 0:01:01AM -> 0:01AM
|
|
307
|
+
* min=h, max=s: 0:00:00AM -> 0AM, 0:00:01AM -> 0:00:01AM, 0:01:01AM -> 0:01:01AM
|
|
308
|
+
*
|
|
309
|
+
* min=m, max=m: 0:00:00AM -> 0:00AM, 0:00:01AM -> 0:00AM, 0:01:01AM -> 0:00AM
|
|
310
|
+
* min=m, max=s: 0:00:00AM -> 0:00AM, 0:00:01AM -> 0:00AM, 0:01:01AM -> 0:01:01AM
|
|
311
|
+
*
|
|
312
|
+
* min=s, max=s: 0:00:00AM -> 0:00:00AM, 0:00:01AM -> 0:00:01AM, 0:01:01AM -> 0:01:01AM
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
let timeFormatted = parts.hour.value;
|
|
316
|
+
|
|
317
|
+
const shouldShowMinutes =
|
|
318
|
+
['minute', 'second'].includes(minPrecision) ||
|
|
319
|
+
(maxPrecision === 'minute' && parts.minute.value !== '00') ||
|
|
320
|
+
(maxPrecision === 'second' && (parts.minute.value !== '00' || parts.second.value !== '00'));
|
|
321
|
+
|
|
322
|
+
if (shouldShowMinutes) {
|
|
323
|
+
timeFormatted += `:${parts.minute.value}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const shouldShowSeconds =
|
|
327
|
+
minPrecision === 'second' || (maxPrecision === 'second' && parts.second.value !== '00');
|
|
328
|
+
|
|
329
|
+
if (shouldShowSeconds) {
|
|
330
|
+
timeFormatted += `:${parts.second.value}`;
|
|
277
331
|
}
|
|
278
332
|
// add the am/pm part
|
|
279
333
|
timeFormatted = `${timeFormatted}${parts.dayPeriod.value.toLowerCase()}`;
|
|
@@ -310,6 +364,8 @@ function formatDateFriendlyParts(
|
|
|
310
364
|
* @param param.timeFirst If true, the time is shown before the date (default false).
|
|
311
365
|
* @param param.dateOnly If true, only the date is shown (default false).
|
|
312
366
|
* @param param.timeOnly If true, only the time is shown (default false).
|
|
367
|
+
* @param param.maxPrecision The maximum precision to show for time (default 'minute').
|
|
368
|
+
* @param param.minPrecision The minimum precision to always show for time (default 'hour').
|
|
313
369
|
* @returns Human-readable string representing the date and time.
|
|
314
370
|
*/
|
|
315
371
|
export function formatDateFriendly(
|
|
@@ -321,18 +377,24 @@ export function formatDateFriendly(
|
|
|
321
377
|
timeFirst = false,
|
|
322
378
|
dateOnly = false,
|
|
323
379
|
timeOnly = false,
|
|
380
|
+
maxPrecision = 'second',
|
|
381
|
+
minPrecision = 'hour',
|
|
324
382
|
}: {
|
|
325
383
|
baseDate?: Date;
|
|
326
384
|
includeTz?: boolean;
|
|
327
385
|
timeFirst?: boolean;
|
|
328
386
|
dateOnly?: boolean;
|
|
329
387
|
timeOnly?: boolean;
|
|
388
|
+
maxPrecision?: TimePrecision;
|
|
389
|
+
minPrecision?: TimePrecision;
|
|
330
390
|
} = {},
|
|
331
391
|
): string {
|
|
332
392
|
const { dateFormatted, timeFormatted, timezoneFormatted } = formatDateFriendlyParts(
|
|
333
393
|
date,
|
|
334
394
|
timezone,
|
|
335
395
|
baseDate,
|
|
396
|
+
maxPrecision,
|
|
397
|
+
minPrecision,
|
|
336
398
|
);
|
|
337
399
|
|
|
338
400
|
let dateTimeFormatted = '';
|
|
@@ -379,15 +441,17 @@ export function formatDateRangeFriendly(
|
|
|
379
441
|
includeTz = true,
|
|
380
442
|
timeFirst = false,
|
|
381
443
|
dateOnly = false,
|
|
444
|
+
maxPrecision = 'second',
|
|
445
|
+
minPrecision = 'hour',
|
|
382
446
|
}: Parameters<typeof formatDateFriendly>[2] = {},
|
|
383
447
|
): string {
|
|
384
448
|
const {
|
|
385
449
|
dateFormatted: startDateFormatted,
|
|
386
450
|
timeFormatted: startTimeFormatted,
|
|
387
451
|
timezoneFormatted,
|
|
388
|
-
} = formatDateFriendlyParts(start, timezone, baseDate);
|
|
452
|
+
} = formatDateFriendlyParts(start, timezone, baseDate, maxPrecision, minPrecision);
|
|
389
453
|
const { dateFormatted: endDateFormatted, timeFormatted: endTimeFormatted } =
|
|
390
|
-
formatDateFriendlyParts(end, timezone, baseDate);
|
|
454
|
+
formatDateFriendlyParts(end, timezone, baseDate, maxPrecision, minPrecision);
|
|
391
455
|
|
|
392
456
|
let result: string | undefined;
|
|
393
457
|
if (dateOnly) {
|