@openmrs/esm-utils 9.0.3-pre.4257 → 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/.turbo/turbo-build.log
CHANGED
package/dist/age-helpers.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
/** @module @category Utility */
|
|
1
2
|
import dayjs from 'dayjs';
|
|
3
|
+
/**
|
|
4
|
+
* Gets the age of a person as a structured duration object, following NHS Digital guidelines
|
|
5
|
+
* (Tables 7 and 8) for which units to include based on the person's age.
|
|
6
|
+
*
|
|
7
|
+
* @see https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
8
|
+
* @param birthDate The birthDate. If null, returns null.
|
|
9
|
+
* @param currentDate Optional. If provided, calculates the age at the provided date instead of now.
|
|
10
|
+
* @returns A DurationInput object, or null if birthDate is null or unparseable.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // For infants, returns fine-grained units
|
|
14
|
+
* ageAsDuration('2024-07-29', '2024-07-30') // => { hours: 24 }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // For adults (>= 18), returns years only
|
|
18
|
+
* ageAsDuration('2000-01-15', '2024-07-30') // => { years: 24 }
|
|
19
|
+
*/
|
|
20
|
+
export declare function ageAsDuration(birthDate: dayjs.ConfigType, currentDate?: dayjs.ConfigType): Intl.DurationInput | null;
|
|
2
21
|
/**
|
|
3
22
|
* Gets a human readable and locale supported representation of a person's age, given their birthDate,
|
|
4
23
|
* The representation logic follows the guideline here:
|
|
@@ -8,6 +27,13 @@ import dayjs from 'dayjs';
|
|
|
8
27
|
* @param birthDate The birthDate. If birthDate is null, returns null.
|
|
9
28
|
* @param currentDate Optional. If provided, calculates the age of the person at the provided currentDate (instead of now).
|
|
10
29
|
* @returns A human-readable string version of the age.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* age('2020-02-29', '2024-07-30') // => '4 yrs, 5 mths'
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // String dates with partial precision are supported
|
|
36
|
+
* age('2000', '2024-07-30') // => '24 yrs'
|
|
11
37
|
*/
|
|
12
38
|
export declare function age(birthDate: dayjs.ConfigType, currentDate?: dayjs.ConfigType): string | null;
|
|
13
39
|
//# sourceMappingURL=age-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"age-helpers.d.ts","sourceRoot":"","sources":["../src/age-helpers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"age-helpers.d.ts","sourceRoot":"","sources":["../src/age-helpers.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,KAAK,CAAC,UAAU,EAC3B,WAAW,GAAE,KAAK,CAAC,UAAoB,GACtC,IAAI,CAAC,aAAa,GAAG,IAAI,CAuC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,GAAE,KAAK,CAAC,UAAoB,GAAG,MAAM,GAAG,IAAI,CAcvG"}
|
package/dist/age-helpers.js
CHANGED
|
@@ -1,50 +1,26 @@
|
|
|
1
|
-
/** @module @category Utility */ import
|
|
2
|
-
import
|
|
3
|
-
import objectSupport from "dayjs/plugin/objectSupport.js";
|
|
4
|
-
import { omit } from "lodash-es";
|
|
5
|
-
import { getLocale } from "./get-locale.js";
|
|
6
|
-
dayjs.extend(objectSupport);
|
|
1
|
+
/** @module @category Utility */ import dayjs from "dayjs";
|
|
2
|
+
import { formatDuration, parseDateInput } from "./dates/date-util.js";
|
|
7
3
|
/**
|
|
8
|
-
* Gets
|
|
9
|
-
*
|
|
10
|
-
* https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
11
|
-
* (See Tables 7 and 8)
|
|
4
|
+
* Gets the age of a person as a structured duration object, following NHS Digital guidelines
|
|
5
|
+
* (Tables 7 and 8) for which units to include based on the person's age.
|
|
12
6
|
*
|
|
13
|
-
* @
|
|
14
|
-
* @param
|
|
15
|
-
* @
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
* @see https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
8
|
+
* @param birthDate The birthDate. If null, returns null.
|
|
9
|
+
* @param currentDate Optional. If provided, calculates the age at the provided date instead of now.
|
|
10
|
+
* @returns A DurationInput object, or null if birthDate is null or unparseable.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // For infants, returns fine-grained units
|
|
14
|
+
* ageAsDuration('2024-07-29', '2024-07-30') // => { hours: 24 }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // For adults (>= 18), returns years only
|
|
18
|
+
* ageAsDuration('2000-01-15', '2024-07-30') // => { years: 24 }
|
|
19
|
+
*/ export function ageAsDuration(birthDate, currentDate = dayjs()) {
|
|
21
20
|
const to = dayjs(currentDate);
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
if (parsedDate.invalid) {
|
|
26
|
-
console.warn(`Could not interpret '${birthDate}' as a date`);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
// hack here but any date interprets 2000-01, etc. as yyyy-dd rather than yyyy-mm
|
|
30
|
-
if (parsedDate.day && !parsedDate.month) {
|
|
31
|
-
parsedDate = Object.assign({}, omit(parsedDate, 'day'), {
|
|
32
|
-
month: parsedDate.day
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
// dayjs' object support uses 0-based months, whereas any-date-parser uses 1-based months
|
|
36
|
-
if (parsedDate.month) {
|
|
37
|
-
parsedDate.month -= 1;
|
|
38
|
-
}
|
|
39
|
-
// in dayjs day is day of week; in any-date-parser, its day of month, so we need to convert them
|
|
40
|
-
if (parsedDate.day) {
|
|
41
|
-
parsedDate = Object.assign({}, omit(parsedDate, 'day'), {
|
|
42
|
-
date: parsedDate.day
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
from = dayjs(to).set(parsedDate);
|
|
46
|
-
} else {
|
|
47
|
-
from = dayjs(birthDate);
|
|
21
|
+
const from = parseDateInput(birthDate, to);
|
|
22
|
+
if (from == null) {
|
|
23
|
+
return null;
|
|
48
24
|
}
|
|
49
25
|
const hourDiff = to.diff(from, 'hours');
|
|
50
26
|
const dayDiff = to.diff(from, 'days');
|
|
@@ -52,16 +28,8 @@ dayjs.extend(objectSupport);
|
|
|
52
28
|
const monthDiff = to.diff(from, 'months');
|
|
53
29
|
const yearDiff = to.diff(from, 'years');
|
|
54
30
|
const duration = {};
|
|
55
|
-
const options = {
|
|
56
|
-
style: 'short',
|
|
57
|
-
localeMatcher: 'lookup'
|
|
58
|
-
};
|
|
59
31
|
if (hourDiff < 2) {
|
|
60
|
-
|
|
61
|
-
duration['minutes'] = minuteDiff;
|
|
62
|
-
if (minuteDiff === 0) {
|
|
63
|
-
options.minutesDisplay = 'always';
|
|
64
|
-
}
|
|
32
|
+
duration['minutes'] = to.diff(from, 'minutes');
|
|
65
33
|
} else if (dayDiff < 2) {
|
|
66
34
|
duration['hours'] = hourDiff;
|
|
67
35
|
} else if (weekDiff < 4) {
|
|
@@ -81,5 +49,35 @@ dayjs.extend(objectSupport);
|
|
|
81
49
|
} else {
|
|
82
50
|
duration['years'] = yearDiff;
|
|
83
51
|
}
|
|
84
|
-
return
|
|
52
|
+
return duration;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Gets a human readable and locale supported representation of a person's age, given their birthDate,
|
|
56
|
+
* The representation logic follows the guideline here:
|
|
57
|
+
* https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
|
|
58
|
+
* (See Tables 7 and 8)
|
|
59
|
+
*
|
|
60
|
+
* @param birthDate The birthDate. If birthDate is null, returns null.
|
|
61
|
+
* @param currentDate Optional. If provided, calculates the age of the person at the provided currentDate (instead of now).
|
|
62
|
+
* @returns A human-readable string version of the age.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* age('2020-02-29', '2024-07-30') // => '4 yrs, 5 mths'
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // String dates with partial precision are supported
|
|
69
|
+
* age('2000', '2024-07-30') // => '24 yrs'
|
|
70
|
+
*/ export function age(birthDate, currentDate = dayjs()) {
|
|
71
|
+
const durationInput = ageAsDuration(birthDate, currentDate);
|
|
72
|
+
if (durationInput == null) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const options = {
|
|
76
|
+
style: 'short',
|
|
77
|
+
localeMatcher: 'lookup'
|
|
78
|
+
};
|
|
79
|
+
if ('minutes' in durationInput && durationInput.minutes === 0) {
|
|
80
|
+
options.minutesDisplay = 'always';
|
|
81
|
+
}
|
|
82
|
+
return formatDuration(durationInput, options);
|
|
85
83
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @category Date and Time
|
|
4
4
|
*/
|
|
5
5
|
import { type CalendarDate, type CalendarDateTime, type CalendarIdentifier, type ZonedDateTime } from '@internationalized/date';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
6
7
|
export type DateInput = string | number | Date;
|
|
7
8
|
/**
|
|
8
9
|
* This function checks whether a date string is the OpenMRS ISO format.
|
|
@@ -167,4 +168,88 @@ export declare function convertToLocaleCalendar(date: CalendarDate | CalendarDat
|
|
|
167
168
|
* @returns The formatted duration string.
|
|
168
169
|
*/
|
|
169
170
|
export declare function formatDuration(duration: Intl.DurationInput, options?: Intl.DurationFormatOptions): string;
|
|
171
|
+
/**
|
|
172
|
+
* Parses a date input into a dayjs object. String inputs are interpreted using
|
|
173
|
+
* any-date-parser with corrections for its month/day representation differences
|
|
174
|
+
* with dayjs. Non-string inputs are passed directly to dayjs.
|
|
175
|
+
*
|
|
176
|
+
* @param dateInput The date to parse.
|
|
177
|
+
* @param referenceDate Used as the base when resolving partial string dates (e.g., '2000' resolves missing fields from this date).
|
|
178
|
+
* @returns A dayjs object, or null if the string could not be parsed.
|
|
179
|
+
*/
|
|
180
|
+
export declare function parseDateInput(dateInput: dayjs.ConfigType, referenceDate: dayjs.Dayjs): dayjs.Dayjs | null;
|
|
181
|
+
type DurationUnitPlural = 'seconds' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
|
|
182
|
+
type DurationUnitSingular = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year';
|
|
183
|
+
/** Accepts both singular ('year') and plural ('years') forms, mirroring Temporal.Duration.round(). */
|
|
184
|
+
export type DurationUnit = DurationUnitPlural | DurationUnitSingular;
|
|
185
|
+
export interface DurationOptions {
|
|
186
|
+
/** Override auto-selection thresholds. Each value is in the unit's own terms (e.g., seconds: 30 means "use seconds if < 30 seconds"). */
|
|
187
|
+
thresholds?: Partial<Record<DurationUnit, number>>;
|
|
188
|
+
/**
|
|
189
|
+
* Coarsest unit to include. Accepts 'auto' (default when smallestUnit is set),
|
|
190
|
+
* which resolves to the largest non-zero unit or smallestUnit, whichever is greater.
|
|
191
|
+
* Mirrors Temporal.Duration.round() behavior.
|
|
192
|
+
*/
|
|
193
|
+
largestUnit?: DurationUnit | 'auto';
|
|
194
|
+
/** Finest unit to include. Defaults to largestUnit when largestUnit is an explicit unit, giving a single-unit result. */
|
|
195
|
+
smallestUnit?: DurationUnit;
|
|
196
|
+
}
|
|
197
|
+
export interface DurationOptionsWithFormat extends DurationOptions {
|
|
198
|
+
/** Options passed to Intl.DurationFormat. Defaults to { style: 'short', localeMatcher: 'lookup' }. */
|
|
199
|
+
formatOptions?: Intl.DurationFormatOptions;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Calculates the duration between two dates as a structured duration object.
|
|
203
|
+
*
|
|
204
|
+
* When called with no options or a single unit string, the unit is auto-selected
|
|
205
|
+
* using dayjs relativeTime thresholds:
|
|
206
|
+
* - < 45 seconds → seconds
|
|
207
|
+
* - < 45 minutes → minutes
|
|
208
|
+
* - < 22 hours → hours
|
|
209
|
+
* - < 26 days → days
|
|
210
|
+
* - < 11 months → months
|
|
211
|
+
* - otherwise → years
|
|
212
|
+
*
|
|
213
|
+
* With a {@link DurationOptions} object, you can override thresholds and/or request
|
|
214
|
+
* a multi-unit decomposition via largestUnit/smallestUnit.
|
|
215
|
+
*
|
|
216
|
+
* @param startDate The start date. If null, returns null.
|
|
217
|
+
* @param endDate Optional. Defaults to now.
|
|
218
|
+
* @param options A unit string for single-unit output, or a DurationOptions object.
|
|
219
|
+
* @returns A DurationInput object, or null if either date is null or unparseable.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* // Auto-selects the appropriate unit
|
|
223
|
+
* duration('2022-01-01', '2024-07-30') // => { years: 2 }
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* // Multi-unit decomposition
|
|
227
|
+
* duration('2022-01-01', '2024-07-30', { largestUnit: 'year', smallestUnit: 'day' })
|
|
228
|
+
* // => { years: 2, months: 6, days: 29 }
|
|
229
|
+
*/
|
|
230
|
+
export declare function duration(startDate: dayjs.ConfigType, endDate?: dayjs.ConfigType, options?: DurationUnit | DurationOptions): Intl.DurationInput | null;
|
|
231
|
+
/**
|
|
232
|
+
* Calculates the duration between two dates and formats it as a locale-aware string.
|
|
233
|
+
* Uses the same unit-selection logic as {@link duration} and delegates formatting
|
|
234
|
+
* to {@link formatDuration}.
|
|
235
|
+
*
|
|
236
|
+
* @param startDate The start date. If null, returns null.
|
|
237
|
+
* @param endDate Optional. Defaults to now.
|
|
238
|
+
* @param options A unit string for single-unit output, or a {@link DurationOptionsWithFormat} object.
|
|
239
|
+
* The `formatOptions` field is passed to Intl.DurationFormat (defaults to short style).
|
|
240
|
+
* @returns A formatted duration string, or null if either date is null or unparseable.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* formatDurationBetween('2022-01-01', '2024-07-30') // => '2 yrs'
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* // Multi-unit with long-form formatting
|
|
247
|
+
* formatDurationBetween('2022-01-01', '2024-07-30', {
|
|
248
|
+
* largestUnit: 'year',
|
|
249
|
+
* smallestUnit: 'day',
|
|
250
|
+
* formatOptions: { style: 'long' },
|
|
251
|
+
* }) // => '2 years, 6 months, 29 days'
|
|
252
|
+
*/
|
|
253
|
+
export declare function formatDurationBetween(startDate: dayjs.ConfigType, endDate?: dayjs.ConfigType, options?: DurationUnit | DurationOptionsWithFormat): string | null;
|
|
254
|
+
export {};
|
|
170
255
|
//# sourceMappingURL=date-util.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-util.d.ts","sourceRoot":"","sources":["../../src/dates/date-util.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAGnB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"date-util.d.ts","sourceRoot":"","sources":["../../src/dates/date-util.ts"],"names":[],"mappings":"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"}
|
package/dist/dates/date-util.js
CHANGED
|
@@ -360,3 +360,227 @@ const formatParts = (separator)=>{
|
|
|
360
360
|
const formatter = new Intl.DurationFormat(getLocale(), options);
|
|
361
361
|
return formatter.format(duration);
|
|
362
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Parses a date input into a dayjs object. String inputs are interpreted using
|
|
365
|
+
* any-date-parser with corrections for its month/day representation differences
|
|
366
|
+
* with dayjs. Non-string inputs are passed directly to dayjs.
|
|
367
|
+
*
|
|
368
|
+
* @param dateInput The date to parse.
|
|
369
|
+
* @param referenceDate Used as the base when resolving partial string dates (e.g., '2000' resolves missing fields from this date).
|
|
370
|
+
* @returns A dayjs object, or null if the string could not be parsed.
|
|
371
|
+
*/ export function parseDateInput(dateInput, referenceDate) {
|
|
372
|
+
if (dateInput == null) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
if (typeof dateInput === 'string') {
|
|
376
|
+
const locale = getLocale();
|
|
377
|
+
let parsedDate = attempt(dateInput, locale);
|
|
378
|
+
if (parsedDate.invalid) {
|
|
379
|
+
console.warn(`Could not interpret '${dateInput}' as a date`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
// hack here but any date interprets 2000-01, etc. as yyyy-dd rather than yyyy-mm
|
|
383
|
+
if (parsedDate.day && !parsedDate.month) {
|
|
384
|
+
parsedDate = {
|
|
385
|
+
...omit(parsedDate, 'day'),
|
|
386
|
+
...{
|
|
387
|
+
month: parsedDate.day
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// dayjs' object support uses 0-based months, whereas any-date-parser uses 1-based months
|
|
392
|
+
if (parsedDate.month) {
|
|
393
|
+
parsedDate.month -= 1;
|
|
394
|
+
}
|
|
395
|
+
// in dayjs day is day of week; in any-date-parser, its day of month, so we need to convert them
|
|
396
|
+
if (parsedDate.day) {
|
|
397
|
+
parsedDate = {
|
|
398
|
+
...omit(parsedDate, 'day'),
|
|
399
|
+
...{
|
|
400
|
+
date: parsedDate.day
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return dayjs(referenceDate).set(parsedDate);
|
|
405
|
+
}
|
|
406
|
+
return dayjs(dateInput);
|
|
407
|
+
}
|
|
408
|
+
const UNIT_ORDER = [
|
|
409
|
+
'years',
|
|
410
|
+
'months',
|
|
411
|
+
'days',
|
|
412
|
+
'hours',
|
|
413
|
+
'minutes',
|
|
414
|
+
'seconds'
|
|
415
|
+
];
|
|
416
|
+
const SINGULAR_TO_PLURAL = {
|
|
417
|
+
second: 'seconds',
|
|
418
|
+
minute: 'minutes',
|
|
419
|
+
hour: 'hours',
|
|
420
|
+
day: 'days',
|
|
421
|
+
month: 'months',
|
|
422
|
+
year: 'years'
|
|
423
|
+
};
|
|
424
|
+
function normalizeUnit(unit) {
|
|
425
|
+
return SINGULAR_TO_PLURAL[unit] ?? unit;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Normalizes threshold keys from singular/plural to plural, then merges with defaults.
|
|
429
|
+
*/ function normalizeThresholds(thresholds) {
|
|
430
|
+
const result = {
|
|
431
|
+
...DEFAULT_THRESHOLDS
|
|
432
|
+
};
|
|
433
|
+
if (thresholds) {
|
|
434
|
+
for (const [key, value] of Object.entries(thresholds)){
|
|
435
|
+
if (value !== undefined) {
|
|
436
|
+
result[normalizeUnit(key)] = value;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
const DEFAULT_THRESHOLDS = {
|
|
443
|
+
seconds: 45,
|
|
444
|
+
minutes: 45,
|
|
445
|
+
hours: 22,
|
|
446
|
+
days: 26,
|
|
447
|
+
months: 11,
|
|
448
|
+
years: Infinity
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Auto-selects the appropriate unit based on the magnitude of the duration,
|
|
452
|
+
* using the provided thresholds (or defaults).
|
|
453
|
+
*/ function autoSelectUnit(from, to, thresholds) {
|
|
454
|
+
const t = normalizeThresholds(thresholds);
|
|
455
|
+
if (to.diff(from, 'seconds') < t.seconds) return 'seconds';
|
|
456
|
+
if (to.diff(from, 'minutes') < t.minutes) return 'minutes';
|
|
457
|
+
if (to.diff(from, 'hours') < t.hours) return 'hours';
|
|
458
|
+
if (to.diff(from, 'days') < t.days) return 'days';
|
|
459
|
+
if (to.diff(from, 'months') < t.months) return 'months';
|
|
460
|
+
return 'years';
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Finds the largest unit with a non-zero diff, or falls back to smallestUnit.
|
|
464
|
+
* Mirrors the Temporal.Duration.round() 'auto' behavior for largestUnit.
|
|
465
|
+
*/ function autoLargestUnit(from, to, smallestUnit) {
|
|
466
|
+
const smallestIdx = UNIT_ORDER.indexOf(smallestUnit);
|
|
467
|
+
for(let i = 0; i < UNIT_ORDER.length; i++){
|
|
468
|
+
const unit = UNIT_ORDER[i];
|
|
469
|
+
if (to.diff(from, unit) > 0) {
|
|
470
|
+
// Return this unit or smallestUnit, whichever is coarser (lower index)
|
|
471
|
+
return i <= smallestIdx ? unit : smallestUnit;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return smallestUnit;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Decomposes the duration between two dates across a range of units, from
|
|
478
|
+
* largestUnit down to smallestUnit. Each unit's value is the remainder after
|
|
479
|
+
* subtracting all larger units.
|
|
480
|
+
*/ function decompose(from, to, largestUnit, smallestUnit) {
|
|
481
|
+
const startIdx = UNIT_ORDER.indexOf(largestUnit);
|
|
482
|
+
const endIdx = UNIT_ORDER.indexOf(smallestUnit);
|
|
483
|
+
const units = UNIT_ORDER.slice(startIdx, endIdx + 1);
|
|
484
|
+
const result = {};
|
|
485
|
+
let current = from;
|
|
486
|
+
for (const unit of units){
|
|
487
|
+
const diff = to.diff(current, unit);
|
|
488
|
+
result[unit] = diff;
|
|
489
|
+
current = current.add(diff, unit);
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Calculates the duration between two dates as a structured duration object.
|
|
495
|
+
*
|
|
496
|
+
* When called with no options or a single unit string, the unit is auto-selected
|
|
497
|
+
* using dayjs relativeTime thresholds:
|
|
498
|
+
* - < 45 seconds → seconds
|
|
499
|
+
* - < 45 minutes → minutes
|
|
500
|
+
* - < 22 hours → hours
|
|
501
|
+
* - < 26 days → days
|
|
502
|
+
* - < 11 months → months
|
|
503
|
+
* - otherwise → years
|
|
504
|
+
*
|
|
505
|
+
* With a {@link DurationOptions} object, you can override thresholds and/or request
|
|
506
|
+
* a multi-unit decomposition via largestUnit/smallestUnit.
|
|
507
|
+
*
|
|
508
|
+
* @param startDate The start date. If null, returns null.
|
|
509
|
+
* @param endDate Optional. Defaults to now.
|
|
510
|
+
* @param options A unit string for single-unit output, or a DurationOptions object.
|
|
511
|
+
* @returns A DurationInput object, or null if either date is null or unparseable.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* // Auto-selects the appropriate unit
|
|
515
|
+
* duration('2022-01-01', '2024-07-30') // => { years: 2 }
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* // Multi-unit decomposition
|
|
519
|
+
* duration('2022-01-01', '2024-07-30', { largestUnit: 'year', smallestUnit: 'day' })
|
|
520
|
+
* // => { years: 2, months: 6, days: 29 }
|
|
521
|
+
*/ export function duration(startDate, endDate = dayjs(), options) {
|
|
522
|
+
const to = dayjs(endDate);
|
|
523
|
+
const from = parseDateInput(startDate, to);
|
|
524
|
+
if (from == null) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
if (typeof options === 'string') {
|
|
528
|
+
const normalized = normalizeUnit(options);
|
|
529
|
+
return {
|
|
530
|
+
[normalized]: to.diff(from, normalized)
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const { thresholds, largestUnit: rawLargest, smallestUnit: rawSmallest } = options ?? {};
|
|
534
|
+
if (rawLargest !== undefined || rawSmallest !== undefined) {
|
|
535
|
+
const smallest = rawSmallest ? normalizeUnit(rawSmallest) : undefined;
|
|
536
|
+
let largest;
|
|
537
|
+
if (rawLargest === 'auto' || rawLargest === undefined) {
|
|
538
|
+
// 'auto' or omitted: resolve to the largest non-zero unit, or smallestUnit, whichever is coarser
|
|
539
|
+
const effectiveSmallest = smallest ?? 'seconds';
|
|
540
|
+
largest = autoLargestUnit(from, to, effectiveSmallest);
|
|
541
|
+
} else {
|
|
542
|
+
largest = normalizeUnit(rawLargest);
|
|
543
|
+
}
|
|
544
|
+
return decompose(from, to, largest, smallest ?? largest);
|
|
545
|
+
}
|
|
546
|
+
const selected = autoSelectUnit(from, to, thresholds);
|
|
547
|
+
return {
|
|
548
|
+
[selected]: to.diff(from, selected)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const DEFAULT_FORMAT_OPTIONS = {
|
|
552
|
+
style: 'short',
|
|
553
|
+
localeMatcher: 'lookup'
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* Calculates the duration between two dates and formats it as a locale-aware string.
|
|
557
|
+
* Uses the same unit-selection logic as {@link duration} and delegates formatting
|
|
558
|
+
* to {@link formatDuration}.
|
|
559
|
+
*
|
|
560
|
+
* @param startDate The start date. If null, returns null.
|
|
561
|
+
* @param endDate Optional. Defaults to now.
|
|
562
|
+
* @param options A unit string for single-unit output, or a {@link DurationOptionsWithFormat} object.
|
|
563
|
+
* The `formatOptions` field is passed to Intl.DurationFormat (defaults to short style).
|
|
564
|
+
* @returns A formatted duration string, or null if either date is null or unparseable.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* formatDurationBetween('2022-01-01', '2024-07-30') // => '2 yrs'
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* // Multi-unit with long-form formatting
|
|
571
|
+
* formatDurationBetween('2022-01-01', '2024-07-30', {
|
|
572
|
+
* largestUnit: 'year',
|
|
573
|
+
* smallestUnit: 'day',
|
|
574
|
+
* formatOptions: { style: 'long' },
|
|
575
|
+
* }) // => '2 years, 6 months, 29 days'
|
|
576
|
+
*/ export function formatDurationBetween(startDate, endDate = dayjs(), options) {
|
|
577
|
+
const durationInput = duration(startDate, endDate, options);
|
|
578
|
+
if (durationInput == null) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
const formatOpts = typeof options === 'object' && options.formatOptions ? {
|
|
582
|
+
...DEFAULT_FORMAT_OPTIONS,
|
|
583
|
+
...options.formatOptions
|
|
584
|
+
} : DEFAULT_FORMAT_OPTIONS;
|
|
585
|
+
return formatDuration(durationInput, formatOpts);
|
|
586
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-utils",
|
|
3
|
-
"version": "9.0.3-pre.
|
|
3
|
+
"version": "9.0.3-pre.4264",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Helper utilities for OpenMRS",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"rxjs": "6.x"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@openmrs/esm-globals": "9.0.3-pre.
|
|
66
|
+
"@openmrs/esm-globals": "9.0.3-pre.4264",
|
|
67
67
|
"@swc/cli": "0.8.0",
|
|
68
68
|
"@swc/core": "1.15.18",
|
|
69
69
|
"@types/lodash-es": "^4.17.12",
|
package/src/age-helpers.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
import type { i18n } from 'i18next';
|
|
4
|
-
import { age } from '.';
|
|
4
|
+
import { age, ageAsDuration } from '.';
|
|
5
5
|
|
|
6
6
|
window.i18next = { language: 'en' } as i18n;
|
|
7
7
|
|
|
@@ -101,3 +101,80 @@ describe('Age Helper', () => {
|
|
|
101
101
|
expect(age(birthDate, now)).toBe(expectedOutput);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
describe('ageAsDuration', () => {
|
|
106
|
+
const now = dayjs('2024-07-30T08:30:55Z');
|
|
107
|
+
|
|
108
|
+
it('returns null for null birthDate', () => {
|
|
109
|
+
expect(ageAsDuration(null)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.each([
|
|
113
|
+
{
|
|
114
|
+
label: 'just born',
|
|
115
|
+
birthDate: now,
|
|
116
|
+
expected: { minutes: 0 },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: 'aged 1 hour 30 minutes',
|
|
120
|
+
birthDate: now.subtract(1, 'hour').subtract(30, 'minutes'),
|
|
121
|
+
expected: { minutes: 90 },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: 'aged 1 day 2 hours 5 minutes',
|
|
125
|
+
birthDate: now.subtract(1, 'day').subtract(2, 'hours').subtract(5, 'minutes'),
|
|
126
|
+
expected: { hours: 26 },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: 'aged 3 days 17 hours 30 minutes',
|
|
130
|
+
birthDate: now.subtract(3, 'days').subtract(17, 'hours').subtract(30, 'minutes'),
|
|
131
|
+
expected: { days: 3 },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: 'aged 29 days 5 hours 2 minutes',
|
|
135
|
+
birthDate: now.subtract(29, 'days').subtract(5, 'hours').subtract(2, 'minutes'),
|
|
136
|
+
expected: { weeks: 4, days: 1 },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: 'aged 1 year 8 days 5 hours',
|
|
140
|
+
birthDate: now.subtract(1, 'year').subtract(8, 'days').subtract(5, 'hours'),
|
|
141
|
+
expected: { months: 12, days: 8 },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: 'aged 4 years 38 days',
|
|
145
|
+
birthDate: now.subtract(4, 'years').subtract(38, 'days').subtract(5, 'hours'),
|
|
146
|
+
expected: { years: 4, months: 1 },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: 'aged 18 years 38 days',
|
|
150
|
+
birthDate: now.subtract(18, 'years').subtract(38, 'days'),
|
|
151
|
+
expected: { years: 18 },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: 'born in 2000 (string)',
|
|
155
|
+
birthDate: '2000',
|
|
156
|
+
expected: { years: 24 },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
label: 'born in June 2020 (string)',
|
|
160
|
+
birthDate: '2020-06',
|
|
161
|
+
expected: { years: 4, months: 1 },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: 'born Feb 29th 2020 (string)',
|
|
165
|
+
birthDate: '2020-02-29',
|
|
166
|
+
expected: { years: 4, months: 5 },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: 'born January 1st 2020 (string)',
|
|
170
|
+
birthDate: '2020-01-01',
|
|
171
|
+
expected: { years: 4, months: 6 },
|
|
172
|
+
},
|
|
173
|
+
])('returns $expected for person $label', ({ birthDate, expected }) => {
|
|
174
|
+
expect(ageAsDuration(birthDate, now)).toEqual(expected);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns null for an invalid string', () => {
|
|
178
|
+
expect(ageAsDuration('not a date', now)).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
});
|