@openmrs/esm-utils 9.0.3-pre.4260 → 9.0.3-pre.4264
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/.turbo/turbo-build.log +1 -1
- package/dist/age-helpers.d.ts +26 -0
- package/dist/age-helpers.d.ts.map +1 -1
- package/dist/age-helpers.js +52 -54
- package/dist/dates/date-util.d.ts +85 -0
- package/dist/dates/date-util.d.ts.map +1 -1
- package/dist/dates/date-util.js +224 -0
- package/package.json +2 -2
- package/src/age-helpers.test.ts +78 -1
- package/src/age-helpers.ts +57 -53
- package/src/dates/date-util.test.ts +295 -7
- package/src/dates/date-util.ts +280 -0
package/src/age-helpers.ts
CHANGED
|
@@ -1,57 +1,33 @@
|
|
|
1
1
|
/** @module @category Utility */
|
|
2
|
-
import { attempt } from 'any-date-parser';
|
|
3
2
|
import dayjs from 'dayjs';
|
|
4
|
-
import
|
|
5
|
-
import { omit } from 'lodash-es';
|
|
6
|
-
import { getLocale } from './get-locale';
|
|
7
|
-
|
|
8
|
-
dayjs.extend(objectSupport);
|
|
3
|
+
import { formatDuration, parseDateInput } from './dates/date-util';
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
|
-
* Gets
|
|
12
|
-
*
|
|
13
|
-
* https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
14
|
-
* (See Tables 7 and 8)
|
|
6
|
+
* Gets the age of a person as a structured duration object, following NHS Digital guidelines
|
|
7
|
+
* (Tables 7 and 8) for which units to include based on the person's age.
|
|
15
8
|
*
|
|
16
|
-
* @
|
|
17
|
-
* @param
|
|
18
|
-
* @
|
|
9
|
+
* @see https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
10
|
+
* @param birthDate The birthDate. If null, returns null.
|
|
11
|
+
* @param currentDate Optional. If provided, calculates the age at the provided date instead of now.
|
|
12
|
+
* @returns A DurationInput object, or null if birthDate is null or unparseable.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // For infants, returns fine-grained units
|
|
16
|
+
* ageAsDuration('2024-07-29', '2024-07-30') // => { hours: 24 }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // For adults (>= 18), returns years only
|
|
20
|
+
* ageAsDuration('2000-01-15', '2024-07-30') // => { years: 24 }
|
|
19
21
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const locale = getLocale();
|
|
26
|
-
|
|
22
|
+
export function ageAsDuration(
|
|
23
|
+
birthDate: dayjs.ConfigType,
|
|
24
|
+
currentDate: dayjs.ConfigType = dayjs(),
|
|
25
|
+
): Intl.DurationInput | null {
|
|
27
26
|
const to = dayjs(currentDate);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (typeof birthDate === 'string') {
|
|
31
|
-
let parsedDate = attempt(birthDate, locale);
|
|
32
|
-
if (parsedDate.invalid) {
|
|
33
|
-
console.warn(`Could not interpret '${birthDate}' as a date`);
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// hack here but any date interprets 2000-01, etc. as yyyy-dd rather than yyyy-mm
|
|
38
|
-
if (parsedDate.day && !parsedDate.month) {
|
|
39
|
-
parsedDate = Object.assign({}, omit(parsedDate, 'day'), { month: parsedDate.day });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// dayjs' object support uses 0-based months, whereas any-date-parser uses 1-based months
|
|
43
|
-
if (parsedDate.month) {
|
|
44
|
-
parsedDate.month -= 1;
|
|
45
|
-
}
|
|
27
|
+
const from = parseDateInput(birthDate, to);
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
parsedDate = Object.assign({}, omit(parsedDate, 'day'), { date: parsedDate.day });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
from = dayjs(to).set(parsedDate);
|
|
53
|
-
} else {
|
|
54
|
-
from = dayjs(birthDate);
|
|
29
|
+
if (from == null) {
|
|
30
|
+
return null;
|
|
55
31
|
}
|
|
56
32
|
|
|
57
33
|
const hourDiff = to.diff(from, 'hours');
|
|
@@ -61,14 +37,9 @@ export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType =
|
|
|
61
37
|
const yearDiff = to.diff(from, 'years');
|
|
62
38
|
|
|
63
39
|
const duration: Intl.DurationInput = {};
|
|
64
|
-
const options: Intl.DurationFormatOptions = { style: 'short', localeMatcher: 'lookup' };
|
|
65
40
|
|
|
66
41
|
if (hourDiff < 2) {
|
|
67
|
-
|
|
68
|
-
duration['minutes'] = minuteDiff;
|
|
69
|
-
if (minuteDiff === 0) {
|
|
70
|
-
options.minutesDisplay = 'always';
|
|
71
|
-
}
|
|
42
|
+
duration['minutes'] = to.diff(from, 'minutes');
|
|
72
43
|
} else if (dayDiff < 2) {
|
|
73
44
|
duration['hours'] = hourDiff;
|
|
74
45
|
} else if (weekDiff < 4) {
|
|
@@ -89,5 +60,38 @@ export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType =
|
|
|
89
60
|
duration['years'] = yearDiff;
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
return
|
|
63
|
+
return duration;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets a human readable and locale supported representation of a person's age, given their birthDate,
|
|
68
|
+
* The representation logic follows the guideline here:
|
|
69
|
+
* https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
70
|
+
* (See Tables 7 and 8)
|
|
71
|
+
*
|
|
72
|
+
* @param birthDate The birthDate. If birthDate is null, returns null.
|
|
73
|
+
* @param currentDate Optional. If provided, calculates the age of the person at the provided currentDate (instead of now).
|
|
74
|
+
* @returns A human-readable string version of the age.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* age('2020-02-29', '2024-07-30') // => '4 yrs, 5 mths'
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // String dates with partial precision are supported
|
|
81
|
+
* age('2000', '2024-07-30') // => '24 yrs'
|
|
82
|
+
*/
|
|
83
|
+
export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType = dayjs()): string | null {
|
|
84
|
+
const durationInput = ageAsDuration(birthDate, currentDate);
|
|
85
|
+
|
|
86
|
+
if (durationInput == null) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const options: Intl.DurationFormatOptions = { style: 'short', localeMatcher: 'lookup' };
|
|
91
|
+
|
|
92
|
+
if ('minutes' in durationInput && durationInput.minutes === 0) {
|
|
93
|
+
options.minutesDisplay = 'always';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return formatDuration(durationInput, options);
|
|
93
97
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
import timezoneMock from 'timezone-mock';
|
|
4
4
|
import type { i18n } from 'i18next';
|
|
@@ -12,11 +12,15 @@ import {
|
|
|
12
12
|
registerDefaultCalendar,
|
|
13
13
|
formatPartialDate,
|
|
14
14
|
formatDuration,
|
|
15
|
+
duration,
|
|
16
|
+
formatDurationBetween,
|
|
15
17
|
} from './date-util';
|
|
16
18
|
|
|
17
19
|
window.i18next = { language: 'en' } as i18n;
|
|
18
20
|
|
|
19
21
|
describe('Openmrs Dates', () => {
|
|
22
|
+
afterAll(() => timezoneMock.unregister());
|
|
23
|
+
|
|
20
24
|
it('converts js Date object to omrs date string version', () => {
|
|
21
25
|
let date = dayjs('2018-03-19T00:05:03.999+0300', 'YYYY-MM-DDTHH:mm:ss.SSSZZ').toDate();
|
|
22
26
|
expect(toOmrsIsoString(date, true)).toEqual('2018-03-18T21:05:03.999+0000');
|
|
@@ -200,13 +204,297 @@ describe('Openmrs Dates', () => {
|
|
|
200
204
|
|
|
201
205
|
it('formats duration with respect to the locale', () => {
|
|
202
206
|
window.i18next.language = 'en';
|
|
203
|
-
const
|
|
204
|
-
expect(formatDuration(
|
|
205
|
-
expect(formatDuration(
|
|
206
|
-
|
|
207
|
-
);
|
|
208
|
-
expect(formatDuration(duration, { style: 'long' })).toMatch(
|
|
207
|
+
const dur = { hours: 1, minutes: 1, seconds: 1, days: 1, months: 1, years: 1 };
|
|
208
|
+
expect(formatDuration(dur, { style: 'narrow' })).toMatch(/1y.*1m.*1d.*1h.*1m.*1s/);
|
|
209
|
+
expect(formatDuration(dur, { style: 'short' })).toMatch(/1\s+yr,.*1\s+mth,.*1\s+day,.*1\s+hr,.*1\s+min,.*1\s+sec/);
|
|
210
|
+
expect(formatDuration(dur, { style: 'long' })).toMatch(
|
|
209
211
|
/1\s+year,.*1\s+month,.*1\s+day,.*1\s+hour,.*1\s+minute,.*1\s+second/,
|
|
210
212
|
);
|
|
211
213
|
});
|
|
212
214
|
});
|
|
215
|
+
|
|
216
|
+
describe('duration', () => {
|
|
217
|
+
const now = dayjs('2024-07-30T08:30:55Z');
|
|
218
|
+
|
|
219
|
+
it('returns null for null startDate', () => {
|
|
220
|
+
expect(duration(null, now)).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('auto-selected units', () => {
|
|
224
|
+
it.each([
|
|
225
|
+
{
|
|
226
|
+
label: '0 seconds ago',
|
|
227
|
+
startDate: now,
|
|
228
|
+
expected: { seconds: 0 },
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
label: '30 seconds ago',
|
|
232
|
+
startDate: now.subtract(30, 'seconds'),
|
|
233
|
+
expected: { seconds: 30 },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
label: '44 seconds ago (still seconds)',
|
|
237
|
+
startDate: now.subtract(44, 'seconds'),
|
|
238
|
+
expected: { seconds: 44 },
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: '45 seconds ago (switches to minutes)',
|
|
242
|
+
startDate: now.subtract(45, 'seconds'),
|
|
243
|
+
expected: { minutes: 0 },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
label: '30 minutes ago',
|
|
247
|
+
startDate: now.subtract(30, 'minutes'),
|
|
248
|
+
expected: { minutes: 30 },
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
label: '44 minutes ago (still minutes)',
|
|
252
|
+
startDate: now.subtract(44, 'minutes'),
|
|
253
|
+
expected: { minutes: 44 },
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
label: '45 minutes ago (switches to hours)',
|
|
257
|
+
startDate: now.subtract(45, 'minutes'),
|
|
258
|
+
expected: { hours: 0 },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
label: '10 hours ago',
|
|
262
|
+
startDate: now.subtract(10, 'hours'),
|
|
263
|
+
expected: { hours: 10 },
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
label: '22 hours ago (switches to days)',
|
|
267
|
+
startDate: now.subtract(22, 'hours'),
|
|
268
|
+
expected: { days: 0 },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
label: '20 days ago',
|
|
272
|
+
startDate: now.subtract(20, 'days'),
|
|
273
|
+
expected: { days: 20 },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
label: '26 days ago (switches to months)',
|
|
277
|
+
startDate: now.subtract(26, 'days'),
|
|
278
|
+
expected: { months: 0 },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
label: '6 months ago',
|
|
282
|
+
startDate: now.subtract(6, 'months'),
|
|
283
|
+
expected: { months: 6 },
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
label: '11 months ago (switches to years)',
|
|
287
|
+
startDate: now.subtract(11, 'months'),
|
|
288
|
+
expected: { years: 0 },
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
label: '3 years ago',
|
|
292
|
+
startDate: now.subtract(3, 'years'),
|
|
293
|
+
expected: { years: 3 },
|
|
294
|
+
},
|
|
295
|
+
])('returns $expected for $label', ({ startDate, expected }) => {
|
|
296
|
+
expect(duration(startDate, now)).toEqual(expected);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('explicit unit', () => {
|
|
301
|
+
it.each([
|
|
302
|
+
{ unit: 'days' as const, expected: { days: 366 } },
|
|
303
|
+
{ unit: 'hours' as const, expected: { hours: 8784 } },
|
|
304
|
+
{ unit: 'months' as const, expected: { months: 12 } },
|
|
305
|
+
{ unit: 'years' as const, expected: { years: 1 } },
|
|
306
|
+
{ unit: 'year' as const, expected: { years: 1 } },
|
|
307
|
+
])('returns $expected for explicit unit "$unit"', ({ unit, expected }) => {
|
|
308
|
+
const oneYearAgo = now.subtract(1, 'year');
|
|
309
|
+
expect(duration(oneYearAgo, now, unit)).toEqual(expected);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('string dates', () => {
|
|
314
|
+
it('handles year-only string', () => {
|
|
315
|
+
expect(duration('2000', now)).toEqual({ years: 24 });
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('handles year-month string', () => {
|
|
319
|
+
expect(duration('2020-06', now)).toEqual({ years: 4 });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('handles full date string', () => {
|
|
323
|
+
expect(duration('2024-07-30', now)).toEqual({ seconds: 0 });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('returns null for invalid string', () => {
|
|
327
|
+
expect(duration('not a date', now)).toBeNull();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('custom thresholds', () => {
|
|
332
|
+
it('uses custom seconds threshold', () => {
|
|
333
|
+
const thirtySecondsAgo = now.subtract(30, 'seconds');
|
|
334
|
+
// Default threshold is 45, so 30s would be seconds. With threshold 20, it should be minutes.
|
|
335
|
+
expect(duration(thirtySecondsAgo, now, { thresholds: { seconds: 20 } })).toEqual({ minutes: 0 });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('uses custom days threshold', () => {
|
|
339
|
+
const twentyDaysAgo = now.subtract(20, 'days');
|
|
340
|
+
// Default threshold is 26, so 20 days would be days. With threshold 15, it should be months.
|
|
341
|
+
expect(duration(twentyDaysAgo, now, { thresholds: { days: 15 } })).toEqual({ months: 0 });
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('multi-unit decomposition', () => {
|
|
346
|
+
it('decomposes years, months, days', () => {
|
|
347
|
+
const start = now.subtract(2, 'years').subtract(3, 'months').subtract(15, 'days');
|
|
348
|
+
const result = duration(start, now, { largestUnit: 'years', smallestUnit: 'days' });
|
|
349
|
+
expect(result).toEqual({ years: 2, months: 3, days: 15 });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('decomposes hours and minutes', () => {
|
|
353
|
+
const start = now.subtract(5, 'hours').subtract(30, 'minutes');
|
|
354
|
+
const result = duration(start, now, { largestUnit: 'hours', smallestUnit: 'minutes' });
|
|
355
|
+
expect(result).toEqual({ hours: 5, minutes: 30 });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('accepts singular unit forms', () => {
|
|
359
|
+
const start = now.subtract(2, 'years').subtract(3, 'months');
|
|
360
|
+
const result = duration(start, now, { largestUnit: 'year', smallestUnit: 'month' });
|
|
361
|
+
expect(result).toEqual({ years: 2, months: 3 });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('single unit when largestUnit === smallestUnit', () => {
|
|
365
|
+
const start = now.subtract(100, 'days');
|
|
366
|
+
const result = duration(start, now, { largestUnit: 'days', smallestUnit: 'days' });
|
|
367
|
+
expect(result).toEqual({ days: 100 });
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('defaults smallestUnit to largestUnit when only largestUnit is given', () => {
|
|
371
|
+
const start = now.subtract(100, 'days');
|
|
372
|
+
const result = duration(start, now, { largestUnit: 'days' });
|
|
373
|
+
expect(result).toEqual({ days: 100 });
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('defaults smallestUnit to largestUnit for single-unit result', () => {
|
|
377
|
+
const start = now.subtract(2, 'years').subtract(3, 'months');
|
|
378
|
+
const result = duration(start, now, { largestUnit: 'months' });
|
|
379
|
+
expect(result).toEqual({ months: 27 });
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('largestUnit auto', () => {
|
|
384
|
+
it('finds the largest non-zero unit', () => {
|
|
385
|
+
const start = now.subtract(3, 'months').subtract(5, 'days');
|
|
386
|
+
const result = duration(start, now, { largestUnit: 'auto', smallestUnit: 'days' });
|
|
387
|
+
expect(result).toEqual({ months: 3, days: 5 });
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('falls back to smallestUnit when diff is zero', () => {
|
|
391
|
+
const result = duration(now, now, { largestUnit: 'auto', smallestUnit: 'seconds' });
|
|
392
|
+
expect(result).toEqual({ seconds: 0 });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('resolves auto when smallestUnit is omitted', () => {
|
|
396
|
+
const start = now.subtract(2, 'hours');
|
|
397
|
+
const result = duration(start, now, { largestUnit: 'auto', smallestUnit: 'minutes' });
|
|
398
|
+
expect(result).toEqual({ hours: 2, minutes: 0 });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('uses auto as default when only smallestUnit is set', () => {
|
|
402
|
+
const start = now.subtract(1, 'year').subtract(3, 'months');
|
|
403
|
+
const result = duration(start, now, { smallestUnit: 'months' });
|
|
404
|
+
expect(result).toEqual({ years: 1, months: 3 });
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('formatDurationBetween', () => {
|
|
410
|
+
const now = dayjs('2024-07-30T08:30:55Z');
|
|
411
|
+
|
|
412
|
+
it('returns null for null startDate', () => {
|
|
413
|
+
expect(formatDurationBetween(null, now)).toBeNull();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it.each([
|
|
417
|
+
{
|
|
418
|
+
label: '30 seconds ago',
|
|
419
|
+
startDate: now.subtract(30, 'seconds'),
|
|
420
|
+
expectedPattern: /30\s+sec/,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
label: '30 minutes ago',
|
|
424
|
+
startDate: now.subtract(30, 'minutes'),
|
|
425
|
+
expectedPattern: /30\s+min/,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
label: '10 hours ago',
|
|
429
|
+
startDate: now.subtract(10, 'hours'),
|
|
430
|
+
expectedPattern: /10\s+hr/,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
label: '20 days ago',
|
|
434
|
+
startDate: now.subtract(20, 'days'),
|
|
435
|
+
expectedPattern: /20\s+days/,
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
label: '6 months ago',
|
|
439
|
+
startDate: now.subtract(6, 'months'),
|
|
440
|
+
expectedPattern: /6\s+mths/,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
label: '3 years ago',
|
|
444
|
+
startDate: now.subtract(3, 'years'),
|
|
445
|
+
expectedPattern: /3\s+yrs/,
|
|
446
|
+
},
|
|
447
|
+
])('formats $label', ({ startDate, expectedPattern }) => {
|
|
448
|
+
window.i18next.language = 'en';
|
|
449
|
+
expect(formatDurationBetween(startDate, now)).toMatch(expectedPattern);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('formats with explicit unit', () => {
|
|
453
|
+
window.i18next.language = 'en';
|
|
454
|
+
const oneYearAgo = now.subtract(1, 'year');
|
|
455
|
+
expect(formatDurationBetween(oneYearAgo, now, 'days')).toMatch(/366\s+days/);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('handles year-only string', () => {
|
|
459
|
+
window.i18next.language = 'en';
|
|
460
|
+
expect(formatDurationBetween('2000', now)).toMatch(/24\s+yrs/);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('handles year-month string', () => {
|
|
464
|
+
window.i18next.language = 'en';
|
|
465
|
+
expect(formatDurationBetween('2020-06', now)).toMatch(/4\s+yrs/);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('returns null for invalid string', () => {
|
|
469
|
+
expect(formatDurationBetween('not a date', now)).toBeNull();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('formats multi-unit decomposition', () => {
|
|
473
|
+
window.i18next.language = 'en';
|
|
474
|
+
const start = now.subtract(2, 'years').subtract(3, 'months').subtract(15, 'days');
|
|
475
|
+
const result = formatDurationBetween(start, now, { largestUnit: 'years', smallestUnit: 'days' });
|
|
476
|
+
expect(result).toMatch(/2\s+yrs.*3\s+mths.*15\s+days/);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('supports custom formatOptions with long style', () => {
|
|
480
|
+
window.i18next.language = 'en';
|
|
481
|
+
const start = now.subtract(2, 'years').subtract(3, 'months');
|
|
482
|
+
const result = formatDurationBetween(start, now, {
|
|
483
|
+
largestUnit: 'years',
|
|
484
|
+
smallestUnit: 'months',
|
|
485
|
+
formatOptions: { style: 'long' },
|
|
486
|
+
});
|
|
487
|
+
expect(result).toMatch(/2\s+years.*3\s+months/);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('supports custom formatOptions with narrow style', () => {
|
|
491
|
+
window.i18next.language = 'en';
|
|
492
|
+
const start = now.subtract(2, 'years').subtract(3, 'months');
|
|
493
|
+
const result = formatDurationBetween(start, now, {
|
|
494
|
+
largestUnit: 'years',
|
|
495
|
+
smallestUnit: 'months',
|
|
496
|
+
formatOptions: { style: 'narrow' },
|
|
497
|
+
});
|
|
498
|
+
expect(result).toMatch(/2y.*3m/);
|
|
499
|
+
});
|
|
500
|
+
});
|