@openmrs/esm-utils 6.2.1-pre.2799 → 6.2.1-pre.2805

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/jest.config.js CHANGED
@@ -3,6 +3,9 @@ module.exports = {
3
3
  transform: {
4
4
  '^.+\\.tsx?$': ['@swc/jest'],
5
5
  },
6
+ moduleNameMapper: {
7
+ 'lodash-es': 'lodash',
8
+ },
6
9
  testEnvironment: 'jsdom',
7
10
  testEnvironmentOptions: {
8
11
  url: 'http://localhost/',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-utils",
3
- "version": "6.2.1-pre.2799",
3
+ "version": "6.2.1-pre.2805",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Helper utilities for OpenMRS",
6
6
  "browser": "dist/openmrs-esm-utils.js",
@@ -39,9 +39,11 @@
39
39
  "access": "public"
40
40
  },
41
41
  "devDependencies": {
42
- "@openmrs/esm-globals": "6.2.1-pre.2799",
42
+ "@openmrs/esm-globals": "6.2.1-pre.2805",
43
+ "@types/lodash-es": "^4.17.12",
43
44
  "@types/semver": "^7.3.4",
44
45
  "dayjs": "^1.10.4",
46
+ "jest": "^29.7.0",
45
47
  "rxjs": "^6.5.3"
46
48
  },
47
49
  "peerDependencies": {
@@ -51,8 +53,10 @@
51
53
  "rxjs": "6.x"
52
54
  },
53
55
  "dependencies": {
54
- "@formatjs/intl-durationformat": "^0.2.4",
55
- "@internationalized/date": "^3.7.0",
56
+ "@formatjs/intl-durationformat": "^0.7.3",
57
+ "@internationalized/date": "^3.5.5",
58
+ "any-date-parser": "^2.0.3",
59
+ "lodash-es": "^4.17.21",
56
60
  "semver": "7.3.2"
57
61
  },
58
62
  "stableVersion": "6.2.0"
@@ -8,32 +8,95 @@ describe('Age Helper', () => {
8
8
  // test cases mostly taken from
9
9
  // https://webarchive.nationalarchives.gov.uk/ukgwa/20160921162509mp_/http://systems.digital.nhs.uk/data/cui/uig/patben.pdf
10
10
  // (Table 8)
11
- const now = dayjs('2024-07-30');
12
- const test0 = now;
13
- const test1 = now.subtract(1, 'hour').subtract(30, 'minutes');
14
- const test2 = now.subtract(1, 'day').subtract(2, 'hours').subtract(5, 'minutes');
15
- const test3 = now.subtract(3, 'days').subtract(17, 'hours').subtract(30, 'minutes');
16
- const test4 = now.subtract(27, 'days').subtract(5, 'hours').subtract(2, 'minutes');
17
- const test5 = now.subtract(28, 'days').subtract(5, 'hours').subtract(2, 'minutes');
18
- const test6 = now.subtract(29, 'days').subtract(5, 'hours').subtract(2, 'minutes');
19
- const test7 = now.subtract(1, 'year').subtract(1, 'day').subtract(5, 'hours');
20
- const test8 = now.subtract(1, 'year').subtract(8, 'day').subtract(5, 'hours');
21
- const test9 = now.subtract(1, 'year').subtract(39, 'day').subtract(5, 'hours');
22
- const test10 = now.subtract(4, 'year').subtract(39, 'day');
23
- const test11 = now.subtract(18, 'year').subtract(39, 'day');
11
+ const now = dayjs('2024-07-30T08:30:55Z');
24
12
 
25
- it('should render durations correctly', () => {
26
- expect(age(test0, now)).toBe('0 min');
27
- expect(age(test1, now)).toBe('90 min');
28
- expect(age(test2, now)).toBe('26 hr');
29
- expect(age(test3, now)).toBe('3 days');
30
- expect(age(test4, now)).toBe('27 days');
31
- expect(age(test5, now)).toBe('4 wks');
32
- expect(age(test6, now)).toBe('4 wks, 1 day');
33
- expect(age(test7, now)).toBe('12 mths, 1 day');
34
- expect(age(test8, now)).toBe('12 mths, 8 days');
35
- expect(age(test9, now)).toBe('13 mths, 9 days');
36
- expect(age(test10, now)).toBe('4 yrs, 1 mth');
37
- expect(age(test11, now)).toBe('18 yrs');
13
+ it.each([
14
+ {
15
+ label: 'just born',
16
+ birthDate: now,
17
+ expectedOutput: '0 min',
18
+ },
19
+ {
20
+ label: 'aged 1 hour 30 minutes',
21
+ birthDate: now.subtract(1, 'hour').subtract(30, 'minutes'),
22
+ expectedOutput: '90 min',
23
+ },
24
+ {
25
+ label: 'aged 1 day 2 hours 5 minutes',
26
+ birthDate: now.subtract(1, 'day').subtract(2, 'hours').subtract(5, 'minutes'),
27
+ expectedOutput: '26 hr',
28
+ },
29
+ {
30
+ label: 'aged 3 days 17 hours 7 minutes',
31
+ birthDate: now.subtract(3, 'days').subtract(17, 'hours').subtract(30, 'minutes'),
32
+ expectedOutput: '3 days',
33
+ },
34
+ {
35
+ label: 'aged 27 days 5 hours 2 minutes',
36
+ birthDate: now.subtract(27, 'days').subtract(5, 'hours').subtract(2, 'minutes'),
37
+ expectedOutput: '27 days',
38
+ },
39
+ {
40
+ label: 'aged 28 days 5 hours 2 minutes',
41
+ birthDate: now.subtract(28, 'days').subtract(5, 'hours').subtract(2, 'minutes'),
42
+ expectedOutput: '4 wks',
43
+ },
44
+ {
45
+ label: 'aged 29 days 5 hours 2 minutes',
46
+ birthDate: now.subtract(29, 'days').subtract(5, 'hours').subtract(2, 'minutes'),
47
+ expectedOutput: '4 wks, 1 day',
48
+ },
49
+ {
50
+ label: 'aged 1 year 1 day 5 hours',
51
+ birthDate: now.subtract(1, 'year').subtract(1, 'day').subtract(5, 'hours'),
52
+ expectedOutput: '12 mths, 1 day',
53
+ },
54
+ {
55
+ label: 'aged 1 year 8 days 5 hours',
56
+ birthDate: now.subtract(1, 'year').subtract(8, 'days').subtract(5, 'hours'),
57
+ expectedOutput: '12 mths, 8 days',
58
+ },
59
+ {
60
+ label: 'aged 1 year 38 days 5 hours',
61
+ birthDate: now.subtract(1, 'year').subtract(38, 'days').subtract(5, 'hours'),
62
+ expectedOutput: '13 mths, 8 days',
63
+ },
64
+ {
65
+ label: 'aged 4 years 38 days',
66
+ birthDate: now.subtract(4, 'years').subtract(38, 'days').subtract(5, 'hours'),
67
+ expectedOutput: '4 yrs, 1 mth',
68
+ },
69
+ {
70
+ label: 'aged 18 years 38 days',
71
+ birthDate: now.subtract(18, 'years').subtract(38, 'days'),
72
+ expectedOutput: '18 yrs',
73
+ },
74
+ {
75
+ label: 'born in 2000',
76
+ birthDate: '2000',
77
+ expectedOutput: '24 yrs',
78
+ },
79
+ {
80
+ label: 'born 10 years, 10 months ago estimated',
81
+ birthDate: '2014',
82
+ expectedOutput: '10 yrs',
83
+ },
84
+ {
85
+ label: 'born in June 2020',
86
+ birthDate: '2020-06',
87
+ expectedOutput: '4 yrs, 1 mth',
88
+ },
89
+ {
90
+ label: 'born Feb 29th 2020',
91
+ birthDate: '2020-02-29',
92
+ expectedOutput: '4 yrs, 5 mths',
93
+ },
94
+ {
95
+ label: 'born January 1st 2020',
96
+ birthDate: '2020-01-01',
97
+ expectedOutput: '4 yrs, 6 mths',
98
+ },
99
+ ])("should produce '$expectedOutput' for person $label", ({ birthDate, expectedOutput }) => {
100
+ expect(age(birthDate, now)).toBe(expectedOutput);
38
101
  });
39
102
  });
@@ -1,8 +1,14 @@
1
1
  /** @module @category Utility */
2
- import dayjs from 'dayjs';
3
2
  import { DurationFormat } from '@formatjs/intl-durationformat';
4
3
  import { type DurationFormatOptions, type DurationInput } from '@formatjs/intl-durationformat/src/types';
4
+ import { attempt } from 'any-date-parser';
5
+ import dayjs from 'dayjs';
6
+ import objectSupport from 'dayjs/plugin/objectSupport';
7
+ import { omit } from 'lodash-es';
5
8
  import { getLocale } from './get-locale';
9
+
10
+ dayjs.extend(objectSupport);
11
+
6
12
  /**
7
13
  * Gets a human readable and locale supported representation of a person's age, given their birthDate,
8
14
  * The representation logic follows the guideline here:
@@ -18,8 +24,37 @@ export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType =
18
24
  return null;
19
25
  }
20
26
 
27
+ const locale = getLocale();
28
+
21
29
  const to = dayjs(currentDate);
22
- const from = dayjs(birthDate);
30
+ let from: dayjs.Dayjs;
31
+
32
+ if (typeof birthDate === 'string') {
33
+ let parsedDate = attempt(birthDate, locale);
34
+ if (parsedDate.invalid) {
35
+ console.warn(`Could not interpret '${birthDate}' as a date`);
36
+ return null;
37
+ }
38
+
39
+ // hack here but any date interprets 2000-01, etc. as yyyy-dd rather than yyyy-mm
40
+ if (parsedDate.day && !parsedDate.month) {
41
+ parsedDate = Object.assign({}, omit(parsedDate, 'day'), { month: parsedDate.day });
42
+ }
43
+
44
+ // dayjs' object support uses 0-based months, whereas any-date-parser uses 1-based months
45
+ if (parsedDate.month) {
46
+ parsedDate.month -= 1;
47
+ }
48
+
49
+ // in dayjs day is day of week; in any-date-parser, its day of month, so we need to convert them
50
+ if (parsedDate.day) {
51
+ parsedDate = Object.assign({}, omit(parsedDate, 'day'), { date: parsedDate.day });
52
+ }
53
+
54
+ from = dayjs(to).set(parsedDate);
55
+ } else {
56
+ from = dayjs(birthDate);
57
+ }
23
58
 
24
59
  const hourDiff = to.diff(from, 'hours');
25
60
  const dayDiff = to.diff(from, 'days');
@@ -28,15 +63,12 @@ export function age(birthDate: dayjs.ConfigType, currentDate: dayjs.ConfigType =
28
63
  const yearDiff = to.diff(from, 'years');
29
64
 
30
65
  const duration: DurationInput = {};
31
-
32
- const locale = getLocale();
33
-
34
66
  const options: DurationFormatOptions = { style: 'short', localeMatcher: 'lookup' };
35
67
 
36
68
  if (hourDiff < 2) {
37
69
  const minuteDiff = to.diff(from, 'minutes');
38
70
  duration['minutes'] = minuteDiff;
39
- if (minuteDiff == 0) {
71
+ if (minuteDiff === 0) {
40
72
  options.minutesDisplay = 'always';
41
73
  }
42
74
  } else if (dayDiff < 2) {
@@ -2,7 +2,6 @@
2
2
  * @module
3
3
  * @category Date and Time
4
4
  */
5
- import type { i18n } from 'i18next';
6
5
  import {
7
6
  type CalendarDateTime,
8
7
  type ZonedDateTime,
@@ -10,19 +9,18 @@ import {
10
9
  toCalendar,
11
10
  type CalendarDate,
12
11
  } from '@internationalized/date';
12
+ import { getLocale } from '@openmrs/esm-utils';
13
+ import { attempt } from 'any-date-parser';
13
14
  import dayjs from 'dayjs';
14
- import utc from 'dayjs/plugin/utc';
15
15
  import isToday from 'dayjs/plugin/isToday';
16
- import { getLocale } from '@openmrs/esm-utils';
16
+ import objectSupport from 'dayjs/plugin/objectSupport';
17
+ import utc from 'dayjs/plugin/utc';
18
+ import type { i18n } from 'i18next';
19
+ import { omit } from 'lodash-es';
17
20
 
18
- dayjs.extend(utc);
19
21
  dayjs.extend(isToday);
20
-
21
- declare global {
22
- interface Window {
23
- i18next: i18n;
24
- }
25
- }
22
+ dayjs.extend(utc);
23
+ dayjs.extend(objectSupport);
26
24
 
27
25
  export type DateInput = string | number | Date;
28
26
 
@@ -98,53 +96,6 @@ export function parseDate(dateString: string) {
98
96
  return dayjs(dateString).toDate();
99
97
  }
100
98
 
101
- export type FormatDateMode = 'standard' | 'wide';
102
-
103
- export type FormatDateOptions = {
104
- /**
105
- * The calendar to use when formatting this date.
106
- */
107
- calendar?: string;
108
- /**
109
- * The locale to use when formatting this date
110
- */
111
- locale?: string;
112
- /**
113
- * - `standard`: "03 Feb 2022"
114
- * - `wide`: "03 — Feb — 2022"
115
- */
116
- mode: FormatDateMode;
117
- /**
118
- * Whether the time should be included in the output always (`true`),
119
- * never (`false`), or only when the input date is today (`for today`).
120
- */
121
- time: true | false | 'for today';
122
- /** Whether to include the day number */
123
- day: boolean;
124
- /** Whether to include the month number */
125
- month: boolean;
126
- /** Whether to include the year */
127
- year: boolean;
128
- /** The unicode numbering system to use */
129
- numberingSystem?: string;
130
- /**
131
- * Disables the special handling of dates that are today. If false
132
- * (the default), then dates that are today will be formatted as "Today"
133
- * in the locale language. If true, then dates that are today will be
134
- * formatted the same as all other dates.
135
- */
136
- noToday: boolean;
137
- };
138
-
139
- const defaultOptions: FormatDateOptions = {
140
- mode: 'standard',
141
- time: 'for today',
142
- day: true,
143
- month: true,
144
- year: true,
145
- noToday: false,
146
- };
147
-
148
99
  /**
149
100
  * Internal cache for per-locale calendars
150
101
  */
@@ -214,6 +165,114 @@ export function getDefaultCalendar(locale: Intl.Locale | string | undefined) {
214
165
  return registeredLocaleCalendars.getCalendar(locale_ instanceof Intl.Locale ? locale_ : new Intl.Locale(locale_));
215
166
  }
216
167
 
168
+ export type FormatDateMode = 'standard' | 'wide';
169
+
170
+ export type FormatDateOptions = {
171
+ /**
172
+ * The calendar to use when formatting this date.
173
+ */
174
+ calendar?: string;
175
+ /**
176
+ * The locale to use when formatting this date
177
+ */
178
+ locale?: string;
179
+ /**
180
+ * - `standard`: "03 Feb 2022"
181
+ * - `wide`: "03 — Feb — 2022"
182
+ */
183
+ mode: FormatDateMode;
184
+ /**
185
+ * Whether the time should be included in the output always (`true`),
186
+ * never (`false`), or only when the input date is today (`for today`).
187
+ */
188
+ time: true | false | 'for today';
189
+ /** Whether to include the day number */
190
+ day: boolean;
191
+ /** Whether to include the month number */
192
+ month: boolean;
193
+ /** Whether to include the year */
194
+ year: boolean;
195
+ /** The unicode numbering system to use */
196
+ numberingSystem?: string;
197
+ /**
198
+ * Disables the special handling of dates that are today. If false
199
+ * (the default), then dates that are today will be formatted as "Today"
200
+ * in the locale language. If true, then dates that are today will be
201
+ * formatted the same as all other dates.
202
+ */
203
+ noToday: boolean;
204
+ };
205
+
206
+ const defaultOptions: FormatDateOptions = {
207
+ mode: 'standard',
208
+ time: 'for today',
209
+ day: true,
210
+ month: true,
211
+ year: true,
212
+ noToday: false,
213
+ };
214
+
215
+ /**
216
+ * Formats the string representing a date, including partial representations of dates, according to the current
217
+ * locale and the given options.
218
+ *
219
+ * Default options:
220
+ * - mode: "standard",
221
+ * - time: "for today",
222
+ * - day: true,
223
+ * - month: true,
224
+ * - year: true
225
+ * - noToday: false
226
+ *
227
+ * If the date is today then "Today" is produced (in the locale language).
228
+ * This behavior can be disabled with `noToday: true`.
229
+ *
230
+ * When time is included, it is appended with a comma and a space. This
231
+ * agrees with the output of `Date.prototype.toLocaleString` for *most*
232
+ * locales.
233
+ */
234
+ // TODO: Shouldn't throw on null input
235
+ export function formatPartialDate(dateString: string, options: Partial<FormatDateOptions> = {}) {
236
+ const locale = getLocale();
237
+ let parsed: ReturnType<typeof attempt> & { date?: number } = attempt(dateString, locale);
238
+
239
+ if (parsed.invalid) {
240
+ console.warn(`Could not parse invalid date '${dateString}'`);
241
+ return null;
242
+ }
243
+
244
+ // hack here but any date interprets 2000-01, etc. as yyyy-dd rather than yyyy-mm
245
+ if (parsed.day && !parsed.month) {
246
+ parsed = Object.assign({}, omit(parsed, 'day'), { month: parsed.day });
247
+ }
248
+
249
+ // dayjs' object support uses 0-based months, whereas any-date-parser uses 1-based months
250
+ if (parsed.month) {
251
+ parsed.month -= 1;
252
+ }
253
+
254
+ // in dayjs day is day of week; in any-date-parser, its day of month, so we need to convert them
255
+ if (parsed.day) {
256
+ parsed = Object.assign({}, omit(parsed, 'day'), { date: parsed.day });
257
+ }
258
+
259
+ const date = dayjs().set(parsed).toDate();
260
+
261
+ if (!parsed.year) {
262
+ options.year = false;
263
+ }
264
+
265
+ if (!parsed.month) {
266
+ options.month = false;
267
+ }
268
+
269
+ if (!parsed.date) {
270
+ options.day = false;
271
+ }
272
+
273
+ return formatDate(date, options);
274
+ }
275
+
217
276
  /**
218
277
  * Formats the input date according to the current locale and the
219
278
  * given options.