@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/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
- // format the time string
277
+ const precisionOrder: TimePrecision[] = ['second', 'minute', 'hour'];
278
+ const maxIndex = precisionOrder.indexOf(maxPrecision);
279
+ const minIndex = precisionOrder.indexOf(minPrecision);
269
280
 
270
- let timeFormatted = '';
271
- if (parts.minute.value === '00' && parts.second.value === '00') {
272
- timeFormatted = `${parts.hour.value}`;
273
- } else if (parts.second.value === '00') {
274
- timeFormatted = `${parts.hour.value}:${parts.minute.value}`;
275
- } else {
276
- timeFormatted = `${parts.hour.value}:${parts.minute.value}:${parts.second.value}`;
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) {