fhir-data-utils-ts 0.6.9 → 0.6.10
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/CHANGELOG.md +7 -0
- package/README.md +6 -0
- package/dist/fhir-temporal-presentation.d.ts +27 -0
- package/dist/fhir-temporal-presentation.js +111 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Recover the useful locale-aware intent of the historical UHC
|
|
6
|
+
internationalization helpers as strict FHIR temporal presentation tools.
|
|
7
|
+
Date-only values retain their calendar day, instants render with the user's
|
|
8
|
+
locale and IANA time zone, editor date/time fields compose a canonical FHIR
|
|
9
|
+
instant with optional seconds defaulted to `00`, and inclusive ranges treat
|
|
10
|
+
an omitted upper bound as the supplied current instant.
|
|
11
|
+
|
|
5
12
|
- Preserve locally entered `Observation.code.text` when a terminology code has
|
|
6
13
|
not yet been confirmed, so profile-aware authoring remains visible and can be
|
|
7
14
|
coded later without inventing a code.
|
package/README.md
CHANGED
|
@@ -98,6 +98,12 @@ derived whole-second value; retain the original timezone-bearing instant and
|
|
|
98
98
|
an independent `Encounter.id`. See
|
|
99
99
|
[`docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md`](docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md).
|
|
100
100
|
|
|
101
|
+
Use the `/fhir-temporal-presentation` entry point at UI boundaries. It formats
|
|
102
|
+
FHIR date, dateTime and instant values with the user's locale and IANA time
|
|
103
|
+
zone, composes separate `YYYY-MM-DD` and `HH:mm[:ss]` editor fields, and
|
|
104
|
+
evaluates inclusive date-time ranges. ISO values remain the wire/storage form;
|
|
105
|
+
they are not user-facing labels.
|
|
106
|
+
|
|
101
107
|
No consumer should copy claim paths or rebuild these structures with literals.
|
|
102
108
|
Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
|
|
103
109
|
`/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type FhirTemporalRange = Readonly<{
|
|
2
|
+
from?: string;
|
|
3
|
+
to?: string;
|
|
4
|
+
}>;
|
|
5
|
+
export type LocalDateTimeParts = Readonly<{
|
|
6
|
+
date: string;
|
|
7
|
+
time: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type ComposeFhirDateTimeInput = LocalDateTimeParts & Readonly<{
|
|
10
|
+
timeZone: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type FhirTemporalFormatOptions = Readonly<{
|
|
13
|
+
timeZone?: string;
|
|
14
|
+
includeTime?: boolean;
|
|
15
|
+
includeSeconds?: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Formats a FHIR date, dateTime or instant for a human in their locale.
|
|
19
|
+
* Wire values remain unchanged; ISO syntax is never used as presentation.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatFhirTemporalForLocale(value: string | Date, locale: string, options?: FhirTemporalFormatOptions): string;
|
|
22
|
+
/** Builds a FHIR instant from separate browser-friendly local date and time fields. */
|
|
23
|
+
export declare function composeFhirDateTimeFromLocalParts(input: ComposeFhirDateTimeInput): string;
|
|
24
|
+
/** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
|
|
25
|
+
export declare function splitFhirDateTimeForInput(value: string | Date, timeZone: string): LocalDateTimeParts;
|
|
26
|
+
/** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
|
|
27
|
+
export declare function isFhirTemporalWithinRange(value: string | Date, range: FhirTemporalRange, now?: Date): boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2
|
+
const TIME_PATTERN = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
3
|
+
/**
|
|
4
|
+
* Formats a FHIR date, dateTime or instant for a human in their locale.
|
|
5
|
+
* Wire values remain unchanged; ISO syntax is never used as presentation.
|
|
6
|
+
*/
|
|
7
|
+
export function formatFhirTemporalForLocale(value, locale, options = {}) {
|
|
8
|
+
const dateOnly = typeof value === 'string' ? DATE_PATTERN.exec(value) : null;
|
|
9
|
+
const includeTime = options.includeTime ?? !dateOnly;
|
|
10
|
+
const date = dateOnly
|
|
11
|
+
? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])))
|
|
12
|
+
: value instanceof Date ? value : new Date(value);
|
|
13
|
+
if (Number.isNaN(date.getTime()))
|
|
14
|
+
throw new RangeError('FHIR temporal value is invalid');
|
|
15
|
+
return new Intl.DateTimeFormat(locale, {
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: dateOnly ? '2-digit' : 'short',
|
|
18
|
+
day: dateOnly ? '2-digit' : 'numeric',
|
|
19
|
+
hour: includeTime ? '2-digit' : undefined,
|
|
20
|
+
minute: includeTime ? '2-digit' : undefined,
|
|
21
|
+
second: includeTime && options.includeSeconds ? '2-digit' : undefined,
|
|
22
|
+
hour12: includeTime ? false : undefined,
|
|
23
|
+
timeZone: dateOnly ? 'UTC' : options.timeZone,
|
|
24
|
+
}).format(date);
|
|
25
|
+
}
|
|
26
|
+
/** Builds a FHIR instant from separate browser-friendly local date and time fields. */
|
|
27
|
+
export function composeFhirDateTimeFromLocalParts(input) {
|
|
28
|
+
const dateMatch = DATE_PATTERN.exec(input.date);
|
|
29
|
+
const timeMatch = TIME_PATTERN.exec(input.time);
|
|
30
|
+
if (!dateMatch || !timeMatch)
|
|
31
|
+
throw new RangeError('Use YYYY-MM-DD and HH:mm or HH:mm:ss');
|
|
32
|
+
const expected = {
|
|
33
|
+
year: Number(dateMatch[1]),
|
|
34
|
+
month: Number(dateMatch[2]),
|
|
35
|
+
day: Number(dateMatch[3]),
|
|
36
|
+
hour: Number(timeMatch[1]),
|
|
37
|
+
minute: Number(timeMatch[2]),
|
|
38
|
+
second: Number(timeMatch[3] ?? '00'),
|
|
39
|
+
};
|
|
40
|
+
assertValidCalendarParts(expected);
|
|
41
|
+
assertTimeZone(input.timeZone);
|
|
42
|
+
const expectedUtc = Date.UTC(expected.year, expected.month - 1, expected.day, expected.hour, expected.minute, expected.second);
|
|
43
|
+
let candidate = expectedUtc;
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
45
|
+
const actual = datePartsInTimeZone(new Date(candidate), input.timeZone);
|
|
46
|
+
const actualUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second);
|
|
47
|
+
candidate += expectedUtc - actualUtc;
|
|
48
|
+
}
|
|
49
|
+
const result = new Date(candidate);
|
|
50
|
+
const roundTrip = datePartsInTimeZone(result, input.timeZone);
|
|
51
|
+
if (Object.keys(expected).some((key) => expected[key] !== roundTrip[key])) {
|
|
52
|
+
throw new RangeError('Local date and time do not exist in the selected time zone');
|
|
53
|
+
}
|
|
54
|
+
return result.toISOString();
|
|
55
|
+
}
|
|
56
|
+
/** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
|
|
57
|
+
export function splitFhirDateTimeForInput(value, timeZone) {
|
|
58
|
+
assertTimeZone(timeZone);
|
|
59
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
60
|
+
if (Number.isNaN(date.getTime()))
|
|
61
|
+
throw new RangeError('FHIR temporal value is invalid');
|
|
62
|
+
const parts = datePartsInTimeZone(date, timeZone);
|
|
63
|
+
return {
|
|
64
|
+
date: `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`,
|
|
65
|
+
time: `${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
|
|
69
|
+
export function isFhirTemporalWithinRange(value, range, now = new Date()) {
|
|
70
|
+
const instant = value instanceof Date ? value : new Date(value);
|
|
71
|
+
const from = range.from ? new Date(range.from) : undefined;
|
|
72
|
+
const to = range.to ? new Date(range.to) : now;
|
|
73
|
+
if ([instant, from, to].some((date) => date && Number.isNaN(date.getTime()))) {
|
|
74
|
+
throw new RangeError('FHIR temporal range is invalid');
|
|
75
|
+
}
|
|
76
|
+
return (!from || instant >= from) && instant <= to;
|
|
77
|
+
}
|
|
78
|
+
function datePartsInTimeZone(date, timeZone) {
|
|
79
|
+
const values = new Map(new Intl.DateTimeFormat('en-CA', {
|
|
80
|
+
timeZone,
|
|
81
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
82
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
|
83
|
+
}).formatToParts(date).map(({ type, value }) => [type, value]));
|
|
84
|
+
return {
|
|
85
|
+
year: Number(values.get('year')),
|
|
86
|
+
month: Number(values.get('month')),
|
|
87
|
+
day: Number(values.get('day')),
|
|
88
|
+
hour: Number(values.get('hour')),
|
|
89
|
+
minute: Number(values.get('minute')),
|
|
90
|
+
second: Number(values.get('second')),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function assertValidCalendarParts(parts) {
|
|
94
|
+
if (parts.hour > 23 || parts.minute > 59 || parts.second > 59)
|
|
95
|
+
throw new RangeError('Local time is invalid');
|
|
96
|
+
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
97
|
+
if (date.getUTCFullYear() !== parts.year || date.getUTCMonth() + 1 !== parts.month || date.getUTCDate() !== parts.day) {
|
|
98
|
+
throw new RangeError('Local date is invalid');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function assertTimeZone(timeZone) {
|
|
102
|
+
try {
|
|
103
|
+
new Intl.DateTimeFormat('en', { timeZone }).format(0);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new RangeError('IANA time zone is invalid');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function pad(value) {
|
|
110
|
+
return String(value).padStart(2, '0');
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhir-data-utils-ts",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -111,6 +111,10 @@
|
|
|
111
111
|
"types": "./dist/health-credential-qr.d.ts",
|
|
112
112
|
"default": "./dist/health-credential-qr.js"
|
|
113
113
|
},
|
|
114
|
+
"./fhir-temporal-presentation": {
|
|
115
|
+
"types": "./dist/fhir-temporal-presentation.d.ts",
|
|
116
|
+
"default": "./dist/fhir-temporal-presentation.js"
|
|
117
|
+
},
|
|
114
118
|
"./research-study-r5": {
|
|
115
119
|
"types": "./dist/research-study-r5.d.ts",
|
|
116
120
|
"default": "./dist/research-study-r5.js"
|