@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/.turbo/turbo-build.log +11 -10
- package/dist/openmrs-esm-utils.js +2 -1
- package/dist/openmrs-esm-utils.js.LICENSE.txt +7 -0
- package/dist/openmrs-esm-utils.js.map +1 -1
- package/jest.config.js +3 -0
- package/package.json +8 -4
- package/src/age-helpers.test.ts +89 -26
- package/src/age-helpers.ts +38 -6
- package/src/dates/date-util.ts +116 -57
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-utils",
|
|
3
|
-
"version": "6.2.1-pre.
|
|
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.
|
|
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.
|
|
55
|
-
"@internationalized/date": "^3.
|
|
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"
|
package/src/age-helpers.test.ts
CHANGED
|
@@ -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(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
});
|
package/src/age-helpers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
71
|
+
if (minuteDiff === 0) {
|
|
40
72
|
options.minutesDisplay = 'always';
|
|
41
73
|
}
|
|
42
74
|
} else if (dayDiff < 2) {
|
package/src/dates/date-util.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|