@openmrs/esm-utils 9.0.3-pre.4260 → 9.0.3-pre.4266

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 13 files with swc (131.56ms)
1
+ [0] Successfully compiled: 13 files with swc (107.01ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -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":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,GAAE,KAAK,CAAC,UAAoB,GAAG,MAAM,GAAG,IAAI,CAyEvG"}
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"}
@@ -1,50 +1,26 @@
1
- /** @module @category Utility */ import { attempt } from "any-date-parser";
2
- import dayjs from "dayjs";
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 a human readable and locale supported representation of a person's age, given their birthDate,
9
- * The representation logic follows the guideline here:
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
- * @param birthDate The birthDate. If birthDate is null, returns null.
14
- * @param currentDate Optional. If provided, calculates the age of the person at the provided currentDate (instead of now).
15
- * @returns A human-readable string version of the age.
16
- */ export function age(birthDate, currentDate = dayjs()) {
17
- if (birthDate == null) {
18
- return null;
19
- }
20
- const locale = getLocale();
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
- let from;
23
- if (typeof birthDate === 'string') {
24
- let parsedDate = attempt(birthDate, locale);
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
- const minuteDiff = to.diff(from, 'minutes');
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 new Intl.DurationFormat(locale, options).format(duration);
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;AAajC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAI/C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAuBnE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,WAE9C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAMtE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,UAAQ,GAAG,MAAM,CAQtE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,QAE3C;AA6CD;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,QAEnF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,MAAM,GAAG,SAAS,kCAI1E;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB;;;OAGG;IACH,IAAI,EAAE,IAAI,GAAG,KAAK,GAAG,WAAW,CAAC;IACjC,wCAAwC;IACxC,GAAG,EAAE,OAAO,CAAC;IACb,0CAA0C;IAC1C,KAAK,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAWF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,iBAuC7F;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,UAkE1E;AAiBD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,UAKpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,UAE5F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,aAAa,EACrD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,mDAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,UAGhG"}
1
+ {"version":3,"file":"date-util.d.ts","sourceRoot":"","sources":["../../src/dates/date-util.ts"],"names":[],"mappings":"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"}
@@ -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.4260",
3
+ "version": "9.0.3-pre.4266",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Helper utilities for OpenMRS",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "types": "dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
13
  },
14
14
  "./mock": {
@@ -63,7 +63,7 @@
63
63
  "rxjs": "6.x"
64
64
  },
65
65
  "devDependencies": {
66
- "@openmrs/esm-globals": "9.0.3-pre.4260",
66
+ "@openmrs/esm-globals": "9.0.3-pre.4266",
67
67
  "@swc/cli": "0.8.0",
68
68
  "@swc/core": "1.15.18",
69
69
  "@types/lodash-es": "^4.17.12",
@@ -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
+ });