@atomic-ehr/fhirpath 0.0.2 → 0.0.3
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/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
|
@@ -0,0 +1,1737 @@
|
|
|
1
|
+
// FHIRPath Temporal Values - Functional Implementation
|
|
2
|
+
// Following ADR-019: Refactor from Classes to Functions and Interfaces
|
|
3
|
+
import { Errors } from '../errors';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Precision System
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface PrecisionInfo {
|
|
10
|
+
level: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond';
|
|
11
|
+
value: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PRECISION_VALUES = {
|
|
15
|
+
year: 4,
|
|
16
|
+
month: 6,
|
|
17
|
+
day: 8,
|
|
18
|
+
hour: 10,
|
|
19
|
+
minute: 12,
|
|
20
|
+
second: 14,
|
|
21
|
+
millisecond: 17,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export const DATE_PRECISIONS = ['year', 'month', 'day'] as const;
|
|
25
|
+
export type DatePrecisionLevel = typeof DATE_PRECISIONS[number];
|
|
26
|
+
|
|
27
|
+
export const TIME_PRECISIONS = ['hour', 'minute', 'second', 'millisecond'] as const;
|
|
28
|
+
export type TimePrecisionLevel = typeof TIME_PRECISIONS[number];
|
|
29
|
+
|
|
30
|
+
export type DateTimePrecisionLevel = PrecisionInfo['level'];
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Time Quantity for Arithmetic
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export type TimeUnit = 'year' | 'month' | 'week' | 'day' |
|
|
37
|
+
'hour' | 'minute' | 'second' | 'millisecond';
|
|
38
|
+
|
|
39
|
+
export interface TimeQuantity {
|
|
40
|
+
readonly value: number;
|
|
41
|
+
readonly unit: TimeUnit;
|
|
42
|
+
readonly isCalendarUnit: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createTimeQuantity(value: number, unit: TimeUnit): TimeQuantity {
|
|
46
|
+
const calendarUnits = new Set(['year', 'years', 'month', 'months', 'week', 'weeks', 'day', 'days']);
|
|
47
|
+
return {
|
|
48
|
+
value,
|
|
49
|
+
unit,
|
|
50
|
+
isCalendarUnit: calendarUnits.has(unit)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Discriminated Union Types with Interfaces
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export interface FHIRDate {
|
|
59
|
+
readonly kind: 'FHIRDate';
|
|
60
|
+
readonly year: number;
|
|
61
|
+
readonly month?: number;
|
|
62
|
+
readonly day?: number;
|
|
63
|
+
readonly precision: PrecisionInfo;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FHIRTime {
|
|
67
|
+
readonly kind: 'FHIRTime';
|
|
68
|
+
readonly hour: number;
|
|
69
|
+
readonly minute?: number;
|
|
70
|
+
readonly second?: number;
|
|
71
|
+
readonly millisecond?: number;
|
|
72
|
+
readonly precision: PrecisionInfo;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FHIRDateTime {
|
|
76
|
+
readonly kind: 'FHIRDateTime';
|
|
77
|
+
readonly year: number;
|
|
78
|
+
readonly month?: number;
|
|
79
|
+
readonly day?: number;
|
|
80
|
+
readonly hour?: number;
|
|
81
|
+
readonly minute?: number;
|
|
82
|
+
readonly second?: number;
|
|
83
|
+
readonly millisecond?: number;
|
|
84
|
+
readonly timezoneOffset?: number;
|
|
85
|
+
readonly precision: PrecisionInfo;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type TemporalValue = FHIRDate | FHIRTime | FHIRDateTime;
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Type Guards
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export function isFHIRDate(value: any): value is FHIRDate {
|
|
95
|
+
return value && typeof value === 'object' && value.kind === 'FHIRDate';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isFHIRTime(value: any): value is FHIRTime {
|
|
99
|
+
return value && typeof value === 'object' && value.kind === 'FHIRTime';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isFHIRDateTime(value: any): value is FHIRDateTime {
|
|
103
|
+
return value && typeof value === 'object' && value.kind === 'FHIRDateTime';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isTemporalValue(value: any): value is TemporalValue {
|
|
107
|
+
return isFHIRDate(value) || isFHIRTime(value) || isFHIRDateTime(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Factory Functions
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export function createDate(year: number, month?: number, day?: number): FHIRDate {
|
|
115
|
+
// Validation
|
|
116
|
+
if (day !== undefined && month === undefined) {
|
|
117
|
+
throw new Error('Month must be present if day is present');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (year < 1 || year > 9999) {
|
|
121
|
+
throw new Error('Year must be between 1 and 9999');
|
|
122
|
+
}
|
|
123
|
+
if (month !== undefined && (month < 1 || month > 12)) {
|
|
124
|
+
throw new Error('Month must be between 1 and 12');
|
|
125
|
+
}
|
|
126
|
+
if (day !== undefined && (day < 1 || day > 31)) {
|
|
127
|
+
throw new Error('Day must be between 1 and 31');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Determine precision
|
|
131
|
+
let level: DatePrecisionLevel;
|
|
132
|
+
if (day !== undefined) {
|
|
133
|
+
level = 'day';
|
|
134
|
+
} else if (month !== undefined) {
|
|
135
|
+
level = 'month';
|
|
136
|
+
} else {
|
|
137
|
+
level = 'year';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
kind: 'FHIRDate',
|
|
142
|
+
year,
|
|
143
|
+
month,
|
|
144
|
+
day,
|
|
145
|
+
precision: {
|
|
146
|
+
level,
|
|
147
|
+
value: PRECISION_VALUES[level]
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createTime(hour: number, minute?: number, second?: number, millisecond?: number): FHIRTime {
|
|
153
|
+
// Validation
|
|
154
|
+
if (second !== undefined && minute === undefined) {
|
|
155
|
+
throw new Error('Minute must be present if second is present');
|
|
156
|
+
}
|
|
157
|
+
if (millisecond !== undefined && second === undefined) {
|
|
158
|
+
throw new Error('Second must be present if millisecond is present');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (hour < 0 || hour > 23) {
|
|
162
|
+
throw new Error('Hour must be between 0 and 23');
|
|
163
|
+
}
|
|
164
|
+
if (minute !== undefined && (minute < 0 || minute > 59)) {
|
|
165
|
+
throw new Error('Minute must be between 0 and 59');
|
|
166
|
+
}
|
|
167
|
+
if (second !== undefined && (second < 0 || second > 59)) {
|
|
168
|
+
throw new Error('Second must be between 0 and 59');
|
|
169
|
+
}
|
|
170
|
+
if (millisecond !== undefined && (millisecond < 0 || millisecond > 999)) {
|
|
171
|
+
throw new Error('Millisecond must be between 0 and 999');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Determine precision
|
|
175
|
+
let level: TimePrecisionLevel;
|
|
176
|
+
if (millisecond !== undefined) {
|
|
177
|
+
level = 'millisecond';
|
|
178
|
+
} else if (second !== undefined) {
|
|
179
|
+
level = 'second';
|
|
180
|
+
} else if (minute !== undefined) {
|
|
181
|
+
level = 'minute';
|
|
182
|
+
} else {
|
|
183
|
+
level = 'hour';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Special case: Time precision values are different
|
|
187
|
+
const timePrecisionValues: Record<TimePrecisionLevel, number> = {
|
|
188
|
+
hour: 4,
|
|
189
|
+
minute: 6,
|
|
190
|
+
second: 8,
|
|
191
|
+
millisecond: 11
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
kind: 'FHIRTime',
|
|
196
|
+
hour,
|
|
197
|
+
minute,
|
|
198
|
+
second,
|
|
199
|
+
millisecond,
|
|
200
|
+
precision: {
|
|
201
|
+
level,
|
|
202
|
+
value: timePrecisionValues[level]
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createDateTime(
|
|
208
|
+
year: number,
|
|
209
|
+
month?: number,
|
|
210
|
+
day?: number,
|
|
211
|
+
hour?: number,
|
|
212
|
+
minute?: number,
|
|
213
|
+
second?: number,
|
|
214
|
+
millisecond?: number,
|
|
215
|
+
timezoneOffset?: number
|
|
216
|
+
): FHIRDateTime {
|
|
217
|
+
// Validation
|
|
218
|
+
if (day !== undefined && month === undefined) {
|
|
219
|
+
throw new Error('Month must be present if day is present');
|
|
220
|
+
}
|
|
221
|
+
if (hour !== undefined && day === undefined) {
|
|
222
|
+
throw new Error('Day must be present if hour is present');
|
|
223
|
+
}
|
|
224
|
+
if (minute !== undefined && hour === undefined) {
|
|
225
|
+
throw new Error('Hour must be present if minute is present');
|
|
226
|
+
}
|
|
227
|
+
if (second !== undefined && minute === undefined) {
|
|
228
|
+
throw new Error('Minute must be present if second is present');
|
|
229
|
+
}
|
|
230
|
+
if (millisecond !== undefined && second === undefined) {
|
|
231
|
+
throw new Error('Second must be present if millisecond is present');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (year < 1 || year > 9999) {
|
|
235
|
+
throw new Error('Year must be between 1 and 9999');
|
|
236
|
+
}
|
|
237
|
+
if (month !== undefined && (month < 1 || month > 12)) {
|
|
238
|
+
throw new Error('Month must be between 1 and 12');
|
|
239
|
+
}
|
|
240
|
+
if (day !== undefined && (day < 1 || day > 31)) {
|
|
241
|
+
throw new Error('Day must be between 1 and 31');
|
|
242
|
+
}
|
|
243
|
+
if (hour !== undefined && (hour < 0 || hour > 23)) {
|
|
244
|
+
throw new Error('Hour must be between 0 and 23');
|
|
245
|
+
}
|
|
246
|
+
if (minute !== undefined && (minute < 0 || minute > 59)) {
|
|
247
|
+
throw new Error('Minute must be between 0 and 59');
|
|
248
|
+
}
|
|
249
|
+
if (second !== undefined && (second < 0 || second > 59)) {
|
|
250
|
+
throw new Error('Second must be between 0 and 59');
|
|
251
|
+
}
|
|
252
|
+
if (millisecond !== undefined && (millisecond < 0 || millisecond > 999)) {
|
|
253
|
+
throw new Error('Millisecond must be between 0 and 999');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Determine precision
|
|
257
|
+
let level: DateTimePrecisionLevel;
|
|
258
|
+
if (millisecond !== undefined) {
|
|
259
|
+
level = 'millisecond';
|
|
260
|
+
} else if (second !== undefined) {
|
|
261
|
+
level = 'second';
|
|
262
|
+
} else if (minute !== undefined) {
|
|
263
|
+
level = 'minute';
|
|
264
|
+
} else if (hour !== undefined) {
|
|
265
|
+
level = 'hour';
|
|
266
|
+
} else if (day !== undefined) {
|
|
267
|
+
level = 'day';
|
|
268
|
+
} else if (month !== undefined) {
|
|
269
|
+
level = 'month';
|
|
270
|
+
} else {
|
|
271
|
+
level = 'year';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
kind: 'FHIRDateTime',
|
|
276
|
+
year,
|
|
277
|
+
month,
|
|
278
|
+
day,
|
|
279
|
+
hour,
|
|
280
|
+
minute,
|
|
281
|
+
second,
|
|
282
|
+
millisecond,
|
|
283
|
+
timezoneOffset,
|
|
284
|
+
precision: {
|
|
285
|
+
level,
|
|
286
|
+
value: PRECISION_VALUES[level]
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Comparison Operations
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
export function equals(a: TemporalValue, b: TemporalValue): boolean | null {
|
|
296
|
+
// Different types are never equal
|
|
297
|
+
if (a.kind !== b.kind) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isFHIRDate(a) && isFHIRDate(b)) {
|
|
302
|
+
// Compare year (always present)
|
|
303
|
+
if (a.year !== b.year) return false;
|
|
304
|
+
|
|
305
|
+
// Compare month - if one has it and other doesn't, return null
|
|
306
|
+
if (a.month !== undefined || b.month !== undefined) {
|
|
307
|
+
if (a.month === undefined || b.month === undefined) return null;
|
|
308
|
+
if (a.month !== b.month) return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Compare day - if one has it and other doesn't, return null
|
|
312
|
+
if (a.day !== undefined || b.day !== undefined) {
|
|
313
|
+
if (a.day === undefined || b.day === undefined) return null;
|
|
314
|
+
if (a.day !== b.day) return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (isFHIRTime(a) && isFHIRTime(b)) {
|
|
321
|
+
// Compare hour (always present)
|
|
322
|
+
if (a.hour !== b.hour) return false;
|
|
323
|
+
|
|
324
|
+
// Compare minute
|
|
325
|
+
if (a.minute !== undefined || b.minute !== undefined) {
|
|
326
|
+
if (a.minute === undefined || b.minute === undefined) return null;
|
|
327
|
+
if (a.minute !== b.minute) return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Compare second/millisecond as a single precision per spec
|
|
331
|
+
if ((a.second !== undefined || a.millisecond !== undefined) ||
|
|
332
|
+
(b.second !== undefined || b.millisecond !== undefined)) {
|
|
333
|
+
const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
|
|
334
|
+
const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
|
|
335
|
+
|
|
336
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
337
|
+
|
|
338
|
+
// Compare as integer milliseconds for exact decimal semantics
|
|
339
|
+
const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
|
|
340
|
+
const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
|
|
341
|
+
if (aMs !== bMs) return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (isFHIRDateTime(a) && isFHIRDateTime(b)) {
|
|
348
|
+
// For DateTime with timezones, must normalize or both be naive
|
|
349
|
+
if (a.timezoneOffset !== undefined && b.timezoneOffset !== undefined) {
|
|
350
|
+
// Both have timezones - need to normalize to UTC for comparison
|
|
351
|
+
const aUtc = normalizeToUTC(a);
|
|
352
|
+
const bUtc = normalizeToUTC(b);
|
|
353
|
+
|
|
354
|
+
// Compare year
|
|
355
|
+
if (aUtc.year !== bUtc.year) return false;
|
|
356
|
+
|
|
357
|
+
// Compare month
|
|
358
|
+
if (aUtc.month !== undefined || bUtc.month !== undefined) {
|
|
359
|
+
if (aUtc.month === undefined || bUtc.month === undefined) return null;
|
|
360
|
+
if (aUtc.month !== bUtc.month) return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Compare day
|
|
364
|
+
if (aUtc.day !== undefined || bUtc.day !== undefined) {
|
|
365
|
+
if (aUtc.day === undefined || bUtc.day === undefined) return null;
|
|
366
|
+
if (aUtc.day !== bUtc.day) return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Compare hour
|
|
370
|
+
if (aUtc.hour !== undefined || bUtc.hour !== undefined) {
|
|
371
|
+
if (aUtc.hour === undefined || bUtc.hour === undefined) return null;
|
|
372
|
+
if (aUtc.hour !== bUtc.hour) return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Compare minute
|
|
376
|
+
if (aUtc.minute !== undefined || bUtc.minute !== undefined) {
|
|
377
|
+
if (aUtc.minute === undefined || bUtc.minute === undefined) return null;
|
|
378
|
+
if (aUtc.minute !== bUtc.minute) return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Compare second/millisecond as single precision
|
|
382
|
+
if ((aUtc.second !== undefined || aUtc.millisecond !== undefined) ||
|
|
383
|
+
(bUtc.second !== undefined || bUtc.millisecond !== undefined)) {
|
|
384
|
+
const aHasSecondPrecision = aUtc.second !== undefined || aUtc.millisecond !== undefined;
|
|
385
|
+
const bHasSecondPrecision = bUtc.second !== undefined || bUtc.millisecond !== undefined;
|
|
386
|
+
|
|
387
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
388
|
+
|
|
389
|
+
const aMs = (aUtc.second ?? 0) * 1000 + (aUtc.millisecond ?? 0);
|
|
390
|
+
const bMs = (bUtc.second ?? 0) * 1000 + (bUtc.millisecond ?? 0);
|
|
391
|
+
if (aMs !== bMs) return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return true;
|
|
395
|
+
} else if (a.timezoneOffset === undefined && b.timezoneOffset === undefined) {
|
|
396
|
+
// Both naive - direct comparison
|
|
397
|
+
// Compare year
|
|
398
|
+
if (a.year !== b.year) return false;
|
|
399
|
+
|
|
400
|
+
// Compare month
|
|
401
|
+
if (a.month !== undefined || b.month !== undefined) {
|
|
402
|
+
if (a.month === undefined || b.month === undefined) return null;
|
|
403
|
+
if (a.month !== b.month) return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Compare day
|
|
407
|
+
if (a.day !== undefined || b.day !== undefined) {
|
|
408
|
+
if (a.day === undefined || b.day === undefined) return null;
|
|
409
|
+
if (a.day !== b.day) return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Compare hour
|
|
413
|
+
if (a.hour !== undefined || b.hour !== undefined) {
|
|
414
|
+
if (a.hour === undefined || b.hour === undefined) return null;
|
|
415
|
+
if (a.hour !== b.hour) return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Compare minute
|
|
419
|
+
if (a.minute !== undefined || b.minute !== undefined) {
|
|
420
|
+
if (a.minute === undefined || b.minute === undefined) return null;
|
|
421
|
+
if (a.minute !== b.minute) return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Compare second/millisecond as single precision
|
|
425
|
+
if ((a.second !== undefined || a.millisecond !== undefined) ||
|
|
426
|
+
(b.second !== undefined || b.millisecond !== undefined)) {
|
|
427
|
+
const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
|
|
428
|
+
const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
|
|
429
|
+
|
|
430
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
431
|
+
|
|
432
|
+
const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
|
|
433
|
+
const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
|
|
434
|
+
if (aMs !== bMs) return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return true;
|
|
438
|
+
} else {
|
|
439
|
+
// One has timezone, one doesn't - can't determine equality
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function equivalent(a: TemporalValue, b: TemporalValue): boolean {
|
|
448
|
+
// Different types are never equivalent
|
|
449
|
+
if (a.kind !== b.kind) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Different precision is false for equivalent
|
|
454
|
+
if (a.precision.value !== b.precision.value) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// For same type and precision, use equals logic
|
|
459
|
+
const result = equals(a, b);
|
|
460
|
+
return result === true; // Convert null to false
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function compare(a: TemporalValue, b: TemporalValue): -1 | 0 | 1 | null {
|
|
464
|
+
// Different types can't be compared
|
|
465
|
+
if (a.kind !== b.kind) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isFHIRDate(a) && isFHIRDate(b)) {
|
|
470
|
+
// Compare year (always present)
|
|
471
|
+
if (a.year !== b.year) return a.year < b.year ? -1 : 1;
|
|
472
|
+
|
|
473
|
+
// Compare month - if one has it and other doesn't, return null
|
|
474
|
+
if (a.month !== undefined || b.month !== undefined) {
|
|
475
|
+
if (a.month === undefined || b.month === undefined) return null;
|
|
476
|
+
if (a.month !== b.month) return a.month < b.month ? -1 : 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Compare day - if one has it and other doesn't, return null
|
|
480
|
+
if (a.day !== undefined || b.day !== undefined) {
|
|
481
|
+
if (a.day === undefined || b.day === undefined) return null;
|
|
482
|
+
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isFHIRTime(a) && isFHIRTime(b)) {
|
|
489
|
+
// Compare hour (always present)
|
|
490
|
+
if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1;
|
|
491
|
+
|
|
492
|
+
// Compare minute - if one has it and other doesn't, return null
|
|
493
|
+
if (a.minute !== undefined || b.minute !== undefined) {
|
|
494
|
+
if (a.minute === undefined || b.minute === undefined) return null;
|
|
495
|
+
if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Compare seconds+milliseconds as a single precision group
|
|
499
|
+
if ((a.second !== undefined || a.millisecond !== undefined) ||
|
|
500
|
+
(b.second !== undefined || b.millisecond !== undefined)) {
|
|
501
|
+
const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
|
|
502
|
+
const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
|
|
503
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
504
|
+
const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
|
|
505
|
+
const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
|
|
506
|
+
if (aMs !== bMs) return aMs < bMs ? -1 : 1;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (isFHIRDateTime(a) && isFHIRDateTime(b)) {
|
|
513
|
+
// Handle timezone comparison
|
|
514
|
+
if (a.timezoneOffset !== undefined && b.timezoneOffset !== undefined) {
|
|
515
|
+
// Both have timezones - normalize to UTC
|
|
516
|
+
const aUtc = normalizeToUTC(a);
|
|
517
|
+
const bUtc = normalizeToUTC(b);
|
|
518
|
+
|
|
519
|
+
// Compare year (always present)
|
|
520
|
+
if (aUtc.year !== bUtc.year) return aUtc.year < bUtc.year ? -1 : 1;
|
|
521
|
+
|
|
522
|
+
// Compare month
|
|
523
|
+
if (aUtc.month !== undefined || bUtc.month !== undefined) {
|
|
524
|
+
if (aUtc.month === undefined || bUtc.month === undefined) return null;
|
|
525
|
+
if (aUtc.month !== bUtc.month) return aUtc.month < bUtc.month ? -1 : 1;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Compare day
|
|
529
|
+
if (aUtc.day !== undefined || bUtc.day !== undefined) {
|
|
530
|
+
if (aUtc.day === undefined || bUtc.day === undefined) return null;
|
|
531
|
+
if (aUtc.day !== bUtc.day) return aUtc.day < bUtc.day ? -1 : 1;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Compare hour
|
|
535
|
+
if (aUtc.hour !== undefined || bUtc.hour !== undefined) {
|
|
536
|
+
if (aUtc.hour === undefined || bUtc.hour === undefined) return null;
|
|
537
|
+
if (aUtc.hour !== bUtc.hour) return aUtc.hour < bUtc.hour ? -1 : 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Compare minute
|
|
541
|
+
if (aUtc.minute !== undefined || bUtc.minute !== undefined) {
|
|
542
|
+
if (aUtc.minute === undefined || bUtc.minute === undefined) return null;
|
|
543
|
+
if (aUtc.minute !== bUtc.minute) return aUtc.minute < bUtc.minute ? -1 : 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Compare seconds+milliseconds as a single precision group
|
|
547
|
+
if ((aUtc.second !== undefined || aUtc.millisecond !== undefined) ||
|
|
548
|
+
(bUtc.second !== undefined || bUtc.millisecond !== undefined)) {
|
|
549
|
+
const aHasSecondPrecision = aUtc.second !== undefined || aUtc.millisecond !== undefined;
|
|
550
|
+
const bHasSecondPrecision = bUtc.second !== undefined || bUtc.millisecond !== undefined;
|
|
551
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
552
|
+
const aMs = (aUtc.second ?? 0) * 1000 + (aUtc.millisecond ?? 0);
|
|
553
|
+
const bMs = (bUtc.second ?? 0) * 1000 + (bUtc.millisecond ?? 0);
|
|
554
|
+
if (aMs !== bMs) return aMs < bMs ? -1 : 1;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return 0;
|
|
558
|
+
} else if (a.timezoneOffset === undefined && b.timezoneOffset === undefined) {
|
|
559
|
+
// Both naive - direct comparison
|
|
560
|
+
// Compare year (always present)
|
|
561
|
+
if (a.year !== b.year) return a.year < b.year ? -1 : 1;
|
|
562
|
+
|
|
563
|
+
// Compare month
|
|
564
|
+
if (a.month !== undefined || b.month !== undefined) {
|
|
565
|
+
if (a.month === undefined || b.month === undefined) return null;
|
|
566
|
+
if (a.month !== b.month) return a.month < b.month ? -1 : 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Compare day
|
|
570
|
+
if (a.day !== undefined || b.day !== undefined) {
|
|
571
|
+
if (a.day === undefined || b.day === undefined) return null;
|
|
572
|
+
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Compare hour
|
|
576
|
+
if (a.hour !== undefined || b.hour !== undefined) {
|
|
577
|
+
if (a.hour === undefined || b.hour === undefined) return null;
|
|
578
|
+
if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Compare minute
|
|
582
|
+
if (a.minute !== undefined || b.minute !== undefined) {
|
|
583
|
+
if (a.minute === undefined || b.minute === undefined) return null;
|
|
584
|
+
if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Compare seconds+milliseconds as a single precision group
|
|
588
|
+
if ((a.second !== undefined || a.millisecond !== undefined) ||
|
|
589
|
+
(b.second !== undefined || b.millisecond !== undefined)) {
|
|
590
|
+
const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
|
|
591
|
+
const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
|
|
592
|
+
if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
|
|
593
|
+
const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
|
|
594
|
+
const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
|
|
595
|
+
if (aMs !== bMs) return aMs < bMs ? -1 : 1;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return 0;
|
|
599
|
+
} else {
|
|
600
|
+
// Mixed timezone state - can't compare
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ============================================================================
|
|
609
|
+
// String Formatting
|
|
610
|
+
// ============================================================================
|
|
611
|
+
|
|
612
|
+
export function toTemporalString(value: TemporalValue): string {
|
|
613
|
+
if (isFHIRDate(value)) {
|
|
614
|
+
return formatDate(value);
|
|
615
|
+
}
|
|
616
|
+
if (isFHIRTime(value)) {
|
|
617
|
+
return formatTime(value);
|
|
618
|
+
}
|
|
619
|
+
if (isFHIRDateTime(value)) {
|
|
620
|
+
return formatDateTime(value);
|
|
621
|
+
}
|
|
622
|
+
return '';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function toFHIRPathLiteral(value: TemporalValue): string {
|
|
626
|
+
if (isFHIRDate(value)) {
|
|
627
|
+
return `@${formatDate(value)}`;
|
|
628
|
+
}
|
|
629
|
+
if (isFHIRTime(value)) {
|
|
630
|
+
return `@T${formatTime(value)}`;
|
|
631
|
+
}
|
|
632
|
+
if (isFHIRDateTime(value)) {
|
|
633
|
+
return `@${formatDateTime(value)}`;
|
|
634
|
+
}
|
|
635
|
+
return '';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function formatDate(date: FHIRDate): string {
|
|
639
|
+
const yearStr = date.year >= 0 && date.year < 10000 ?
|
|
640
|
+
String(date.year).padStart(4, '0') :
|
|
641
|
+
String(date.year);
|
|
642
|
+
|
|
643
|
+
if (date.precision.level === 'year') {
|
|
644
|
+
return yearStr;
|
|
645
|
+
} else if (date.precision.level === 'month') {
|
|
646
|
+
return `${yearStr}-${String(date.month).padStart(2, '0')}`;
|
|
647
|
+
} else {
|
|
648
|
+
return `${yearStr}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function formatTime(time: FHIRTime): string {
|
|
653
|
+
let result = String(time.hour).padStart(2, '0');
|
|
654
|
+
|
|
655
|
+
if (time.minute !== undefined) {
|
|
656
|
+
result += ':' + String(time.minute).padStart(2, '0');
|
|
657
|
+
|
|
658
|
+
if (time.second !== undefined) {
|
|
659
|
+
result += ':' + String(time.second).padStart(2, '0');
|
|
660
|
+
|
|
661
|
+
if (time.millisecond !== undefined) {
|
|
662
|
+
result += '.' + String(time.millisecond).padStart(3, '0');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function formatDateTime(dt: FHIRDateTime): string {
|
|
671
|
+
const yearStr = dt.year >= 0 && dt.year < 10000 ?
|
|
672
|
+
String(dt.year).padStart(4, '0') :
|
|
673
|
+
String(dt.year);
|
|
674
|
+
|
|
675
|
+
let result = yearStr;
|
|
676
|
+
|
|
677
|
+
if (dt.month !== undefined) {
|
|
678
|
+
result += '-' + String(dt.month).padStart(2, '0');
|
|
679
|
+
|
|
680
|
+
if (dt.day !== undefined) {
|
|
681
|
+
result += '-' + String(dt.day).padStart(2, '0');
|
|
682
|
+
|
|
683
|
+
if (dt.hour !== undefined) {
|
|
684
|
+
result += 'T' + String(dt.hour).padStart(2, '0');
|
|
685
|
+
|
|
686
|
+
if (dt.minute !== undefined) {
|
|
687
|
+
result += ':' + String(dt.minute).padStart(2, '0');
|
|
688
|
+
|
|
689
|
+
if (dt.second !== undefined) {
|
|
690
|
+
result += ':' + String(dt.second).padStart(2, '0');
|
|
691
|
+
|
|
692
|
+
if (dt.millisecond !== undefined) {
|
|
693
|
+
result += '.' + String(dt.millisecond).padStart(3, '0');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Add timezone
|
|
699
|
+
if (dt.timezoneOffset !== undefined) {
|
|
700
|
+
if (dt.timezoneOffset === 0) {
|
|
701
|
+
result += 'Z';
|
|
702
|
+
} else {
|
|
703
|
+
const sign = dt.timezoneOffset < 0 ? '-' : '+';
|
|
704
|
+
const absOffset = Math.abs(dt.timezoneOffset);
|
|
705
|
+
const hours = Math.floor(absOffset / 60);
|
|
706
|
+
const minutes = absOffset % 60;
|
|
707
|
+
result += sign + String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0');
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// No T suffix when we have complete date (year-month-day) but no time
|
|
712
|
+
} else {
|
|
713
|
+
// Month precision with T suffix (partial date)
|
|
714
|
+
result += 'T';
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
// Year precision with T suffix (partial date)
|
|
718
|
+
result += 'T';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ============================================================================
|
|
725
|
+
// Parsing
|
|
726
|
+
// ============================================================================
|
|
727
|
+
|
|
728
|
+
export function parseTemporalLiteral(literal: string): TemporalValue {
|
|
729
|
+
if (!literal.startsWith('@')) {
|
|
730
|
+
throw new Error('Temporal literal must start with @');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const value = literal.substring(1);
|
|
734
|
+
|
|
735
|
+
// Time literal: @T...
|
|
736
|
+
if (value.startsWith('T')) {
|
|
737
|
+
return parseTimeLiteral(value.substring(1));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Check for DateTime vs Date
|
|
741
|
+
const hasTimeComponent = /T\d/.test(value);
|
|
742
|
+
|
|
743
|
+
if (hasTimeComponent) {
|
|
744
|
+
return parseDateTimeLiteral(value);
|
|
745
|
+
} else if (value.endsWith('T')) {
|
|
746
|
+
// DateTime with only date precision
|
|
747
|
+
return parseDateTimeLiteral(value.slice(0, -1));
|
|
748
|
+
} else {
|
|
749
|
+
// Date literal
|
|
750
|
+
return parseDateLiteral(value);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function parseDateLiteral(value: string): FHIRDate {
|
|
755
|
+
const match = value.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/);
|
|
756
|
+
if (!match) {
|
|
757
|
+
throw new Error(`Invalid date literal: @${value}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const year = parseInt(match[1]!, 10);
|
|
761
|
+
const month = match[2] ? parseInt(match[2]!, 10) : undefined;
|
|
762
|
+
const day = match[3] ? parseInt(match[3]!, 10) : undefined;
|
|
763
|
+
|
|
764
|
+
return createDate(year, month, day);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function parseTimeLiteral(value: string): FHIRTime {
|
|
768
|
+
const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?)?$/);
|
|
769
|
+
if (!match) {
|
|
770
|
+
throw new Error(`Invalid time literal: @T${value}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const hour = parseInt(match[1]!, 10);
|
|
774
|
+
const minute = match[2] ? parseInt(match[2]!, 10) : undefined;
|
|
775
|
+
const second = match[3] ? parseInt(match[3]!, 10) : undefined;
|
|
776
|
+
const millisecond = match[4] ? parseInt(match[4]!, 10) : undefined;
|
|
777
|
+
|
|
778
|
+
return createTime(hour, minute, second, millisecond);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function parseDateTimeLiteral(value: string): FHIRDateTime {
|
|
782
|
+
// Remove trailing T if present (for date-only DateTime)
|
|
783
|
+
const cleanValue = value.endsWith('T') ? value.slice(0, -1) : value;
|
|
784
|
+
|
|
785
|
+
// Split into date and time parts
|
|
786
|
+
const parts = cleanValue.split('T');
|
|
787
|
+
const datePart = parts[0];
|
|
788
|
+
const timePart = parts[1];
|
|
789
|
+
|
|
790
|
+
// Parse date part
|
|
791
|
+
const dateMatch = datePart?.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/);
|
|
792
|
+
if (!dateMatch) {
|
|
793
|
+
throw new Error(`Invalid datetime literal: @${value}`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const year = parseInt(dateMatch[1]!, 10);
|
|
797
|
+
const month = dateMatch[2] ? parseInt(dateMatch[2]!, 10) : undefined;
|
|
798
|
+
const day = dateMatch[3] ? parseInt(dateMatch[3]!, 10) : undefined;
|
|
799
|
+
|
|
800
|
+
let hour: number | undefined;
|
|
801
|
+
let minute: number | undefined;
|
|
802
|
+
let second: number | undefined;
|
|
803
|
+
let millisecond: number | undefined;
|
|
804
|
+
let timezoneOffset: number | undefined;
|
|
805
|
+
|
|
806
|
+
// Parse time part if present
|
|
807
|
+
if (timePart) {
|
|
808
|
+
// Extract timezone
|
|
809
|
+
let timeWithoutTz = timePart;
|
|
810
|
+
const tzMatch = timePart.match(/(Z|[+-]\d{2}:\d{2})$/);
|
|
811
|
+
|
|
812
|
+
if (tzMatch) {
|
|
813
|
+
timeWithoutTz = timePart.substring(0, timePart.length - tzMatch[0].length);
|
|
814
|
+
const tz = tzMatch[0];
|
|
815
|
+
|
|
816
|
+
if (tz === 'Z') {
|
|
817
|
+
timezoneOffset = 0;
|
|
818
|
+
} else {
|
|
819
|
+
const tzParts = tz.match(/([+-])(\d{2}):(\d{2})/);
|
|
820
|
+
if (tzParts) {
|
|
821
|
+
const sign = tzParts[1] === '+' ? 1 : -1;
|
|
822
|
+
const hours = parseInt(tzParts[2]!, 10);
|
|
823
|
+
const minutes = parseInt(tzParts[3]!, 10);
|
|
824
|
+
timezoneOffset = sign * (hours * 60 + minutes);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Parse time components
|
|
830
|
+
const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?)?$/);
|
|
831
|
+
if (timeMatch) {
|
|
832
|
+
hour = parseInt(timeMatch[1]!, 10);
|
|
833
|
+
minute = timeMatch[2] ? parseInt(timeMatch[2]!, 10) : undefined;
|
|
834
|
+
second = timeMatch[3] ? parseInt(timeMatch[3]!, 10) : undefined;
|
|
835
|
+
millisecond = timeMatch[4] ? parseInt(timeMatch[4]!, 10) : undefined;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ============================================================================
|
|
843
|
+
// Arithmetic Operations
|
|
844
|
+
// ============================================================================
|
|
845
|
+
|
|
846
|
+
const DAYS_PER_MONTH = 30;
|
|
847
|
+
const DAYS_PER_YEAR = 365;
|
|
848
|
+
const DAYS_PER_WEEK = 7;
|
|
849
|
+
const HOURS_PER_DAY = 24;
|
|
850
|
+
const MINUTES_PER_HOUR = 60;
|
|
851
|
+
const SECONDS_PER_MINUTE = 60;
|
|
852
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
853
|
+
|
|
854
|
+
function normalizeUnit(unit: string): string {
|
|
855
|
+
// Handle UCUM units
|
|
856
|
+
const ucumMap: Record<string, string> = {
|
|
857
|
+
// 'a' (annum) is not supported for temporal arithmetic - use 'year' keyword
|
|
858
|
+
'mo': 'month', // month
|
|
859
|
+
'wk': 'week', // week
|
|
860
|
+
'd': 'day', // day
|
|
861
|
+
'h': 'hour', // hour
|
|
862
|
+
'min': 'minute', // minute
|
|
863
|
+
's': 'second', // second
|
|
864
|
+
'ms': 'millisecond' // millisecond
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
if (ucumMap[unit]) {
|
|
868
|
+
return ucumMap[unit];
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Remove plurals for regular units
|
|
872
|
+
if (unit.endsWith('s') && unit !== 's' && unit !== 'ms') {
|
|
873
|
+
return unit.slice(0, -1);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return unit;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function convertAndTruncate(quantity: TimeQuantity, targetUnit: string): number {
|
|
880
|
+
const normalizedQuantityUnit = normalizeUnit(quantity.unit);
|
|
881
|
+
|
|
882
|
+
// Direct conversion for calendar units (year <-> month)
|
|
883
|
+
if (normalizedQuantityUnit === 'month' && targetUnit === 'year') {
|
|
884
|
+
return Math.trunc(quantity.value / 12);
|
|
885
|
+
}
|
|
886
|
+
if (normalizedQuantityUnit === 'year' && targetUnit === 'month') {
|
|
887
|
+
return Math.trunc(quantity.value * 12);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
let valueInDays = 0;
|
|
891
|
+
|
|
892
|
+
// Convert to days first
|
|
893
|
+
switch (normalizedQuantityUnit) {
|
|
894
|
+
case 'year':
|
|
895
|
+
valueInDays = quantity.value * DAYS_PER_YEAR;
|
|
896
|
+
break;
|
|
897
|
+
case 'month':
|
|
898
|
+
valueInDays = quantity.value * DAYS_PER_MONTH;
|
|
899
|
+
break;
|
|
900
|
+
case 'week':
|
|
901
|
+
valueInDays = quantity.value * DAYS_PER_WEEK;
|
|
902
|
+
break;
|
|
903
|
+
case 'day':
|
|
904
|
+
valueInDays = quantity.value;
|
|
905
|
+
break;
|
|
906
|
+
case 'hour':
|
|
907
|
+
valueInDays = quantity.value / HOURS_PER_DAY;
|
|
908
|
+
break;
|
|
909
|
+
case 'minute':
|
|
910
|
+
valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR);
|
|
911
|
+
break;
|
|
912
|
+
case 'second':
|
|
913
|
+
valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
|
|
914
|
+
break;
|
|
915
|
+
case 'millisecond':
|
|
916
|
+
valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Convert from days to target unit - use Math.trunc for truncation towards zero
|
|
921
|
+
switch (targetUnit) {
|
|
922
|
+
case 'year':
|
|
923
|
+
return Math.trunc(valueInDays / DAYS_PER_YEAR);
|
|
924
|
+
case 'month':
|
|
925
|
+
return Math.trunc(valueInDays / DAYS_PER_MONTH);
|
|
926
|
+
case 'day':
|
|
927
|
+
return Math.trunc(valueInDays);
|
|
928
|
+
case 'hour':
|
|
929
|
+
return Math.trunc(valueInDays * HOURS_PER_DAY);
|
|
930
|
+
case 'minute':
|
|
931
|
+
return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR);
|
|
932
|
+
case 'second':
|
|
933
|
+
return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
|
|
934
|
+
case 'millisecond':
|
|
935
|
+
return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND);
|
|
936
|
+
default:
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function add(temporal: TemporalValue, quantity: TimeQuantity): TemporalValue {
|
|
942
|
+
if (isFHIRDate(temporal)) {
|
|
943
|
+
return addToDate(temporal, quantity);
|
|
944
|
+
}
|
|
945
|
+
if (isFHIRTime(temporal)) {
|
|
946
|
+
return addToTime(temporal, quantity);
|
|
947
|
+
}
|
|
948
|
+
if (isFHIRDateTime(temporal)) {
|
|
949
|
+
return addToDateTime(temporal, quantity);
|
|
950
|
+
}
|
|
951
|
+
return temporal;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function subtract(temporal: TemporalValue, quantity: TimeQuantity): TemporalValue {
|
|
955
|
+
const negativeQuantity = createTimeQuantity(-quantity.value, quantity.unit);
|
|
956
|
+
return add(temporal, negativeQuantity);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Helper to get the maximum day for a given month/year
|
|
960
|
+
export function getDaysInMonth(year: number, month: number): number {
|
|
961
|
+
if (month === 2) {
|
|
962
|
+
// February - check for leap year
|
|
963
|
+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
|
964
|
+
return isLeapYear ? 29 : 28;
|
|
965
|
+
} else if ([4, 6, 9, 11].includes(month)) {
|
|
966
|
+
return 30;
|
|
967
|
+
} else {
|
|
968
|
+
return 31;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Helper to clamp a day to the valid range for a month
|
|
973
|
+
function clampDay(year: number, month: number | undefined, day: number | undefined): number | undefined {
|
|
974
|
+
if (day === undefined || month === undefined) {
|
|
975
|
+
return day;
|
|
976
|
+
}
|
|
977
|
+
const maxDay = getDaysInMonth(year, month);
|
|
978
|
+
return Math.min(day, maxDay);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function addToDate(date: FHIRDate, quantity: TimeQuantity): FHIRDate {
|
|
982
|
+
// Check for unsupported UCUM units
|
|
983
|
+
if ((quantity.unit as string) === 'a') {
|
|
984
|
+
throw Errors.invalidTemporalUnit('Date', quantity.unit as string);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const normalizedUnit = normalizeUnit(quantity.unit);
|
|
988
|
+
|
|
989
|
+
// Only year/month/week/day units allowed for Date per spec
|
|
990
|
+
if (!['year', 'month', 'week', 'day'].includes(normalizedUnit)) {
|
|
991
|
+
// Includes hour/minute/second/millisecond and any other non-calendar unit
|
|
992
|
+
throw Errors.unsupportedTemporalUnitForType('Date', quantity.unit as string);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const cal = addCalendarParts(
|
|
996
|
+
date.year,
|
|
997
|
+
date.month,
|
|
998
|
+
date.day,
|
|
999
|
+
normalizedUnit as CalendarUnit,
|
|
1000
|
+
quantity.value
|
|
1001
|
+
);
|
|
1002
|
+
return createDate(cal.year, cal.month, cal.day);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function addToTime(time: FHIRTime, quantity: TimeQuantity): FHIRTime {
|
|
1006
|
+
// Check for unsupported UCUM units
|
|
1007
|
+
if ((quantity.unit as string) === 'a') {
|
|
1008
|
+
throw new Error("Cannot use variable-duration unit 'a' with Time - use calendar duration keywords instead");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const normalizedUnit = normalizeUnit(quantity.unit);
|
|
1012
|
+
|
|
1013
|
+
// Only time units allowed
|
|
1014
|
+
if (!['hour', 'minute', 'second', 'millisecond'].includes(normalizedUnit)) {
|
|
1015
|
+
throw new Error(`Cannot add ${quantity.unit} to Time`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Convert everything to milliseconds
|
|
1019
|
+
let totalMs = time.hour * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1020
|
+
|
|
1021
|
+
if (time.minute !== undefined) {
|
|
1022
|
+
totalMs += time.minute * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1023
|
+
}
|
|
1024
|
+
if (time.second !== undefined) {
|
|
1025
|
+
totalMs += time.second * MILLISECONDS_PER_SECOND;
|
|
1026
|
+
}
|
|
1027
|
+
if (time.millisecond !== undefined) {
|
|
1028
|
+
totalMs += time.millisecond;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Add the quantity in milliseconds
|
|
1032
|
+
let quantityMs = 0;
|
|
1033
|
+
switch (normalizedUnit) {
|
|
1034
|
+
case 'hour':
|
|
1035
|
+
// For precisions above seconds, ignore decimal portion per spec
|
|
1036
|
+
quantityMs = Math.trunc(quantity.value) * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1037
|
+
break;
|
|
1038
|
+
case 'minute':
|
|
1039
|
+
// For precisions above seconds, ignore decimal portion per spec
|
|
1040
|
+
quantityMs = Math.trunc(quantity.value) * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1041
|
+
break;
|
|
1042
|
+
case 'second':
|
|
1043
|
+
quantityMs = quantity.value * MILLISECONDS_PER_SECOND;
|
|
1044
|
+
break;
|
|
1045
|
+
case 'millisecond':
|
|
1046
|
+
quantityMs = quantity.value;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
totalMs += quantityMs;
|
|
1051
|
+
|
|
1052
|
+
// Wrap around 24 hours
|
|
1053
|
+
const dayMs = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1054
|
+
totalMs = totalMs % dayMs;
|
|
1055
|
+
if (totalMs < 0) {
|
|
1056
|
+
totalMs += dayMs;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Convert back to components
|
|
1060
|
+
const newHour = Math.floor(totalMs / (MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND));
|
|
1061
|
+
totalMs %= MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1062
|
+
|
|
1063
|
+
let newMinute: number | undefined;
|
|
1064
|
+
let newSecond: number | undefined;
|
|
1065
|
+
let newMillisecond: number | undefined;
|
|
1066
|
+
|
|
1067
|
+
if (time.minute !== undefined) {
|
|
1068
|
+
newMinute = Math.floor(totalMs / (SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND));
|
|
1069
|
+
totalMs %= SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (time.second !== undefined) {
|
|
1073
|
+
newSecond = Math.floor(totalMs / MILLISECONDS_PER_SECOND);
|
|
1074
|
+
totalMs %= MILLISECONDS_PER_SECOND;
|
|
1075
|
+
|
|
1076
|
+
// Include milliseconds if original had them OR if we have fractional seconds
|
|
1077
|
+
if (time.millisecond !== undefined || totalMs > 0) {
|
|
1078
|
+
newMillisecond = Math.floor(totalMs);
|
|
1079
|
+
}
|
|
1080
|
+
} else if (time.millisecond !== undefined) {
|
|
1081
|
+
newMillisecond = Math.floor(totalMs);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return createTime(newHour, newMinute, newSecond, newMillisecond);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function addToDateTime(dt: FHIRDateTime, quantity: TimeQuantity): FHIRDateTime {
|
|
1088
|
+
// Check for unsupported UCUM units
|
|
1089
|
+
if ((quantity.unit as string) === 'a') {
|
|
1090
|
+
throw new Error("Cannot use variable-duration unit 'a' with DateTime - use calendar duration keywords instead");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const normalizedUnit = normalizeUnit(quantity.unit);
|
|
1094
|
+
let newYear = dt.year;
|
|
1095
|
+
let newMonth = dt.month;
|
|
1096
|
+
let newDay = dt.day;
|
|
1097
|
+
let newHour = dt.hour;
|
|
1098
|
+
let newMinute = dt.minute;
|
|
1099
|
+
let newSecond = dt.second;
|
|
1100
|
+
let newMillisecond = dt.millisecond;
|
|
1101
|
+
|
|
1102
|
+
// Calendar units: delegate to shared helper
|
|
1103
|
+
if (['year', 'month', 'week', 'day'].includes(normalizedUnit)) {
|
|
1104
|
+
const cal = addCalendarParts(newYear, newMonth, newDay, normalizedUnit as CalendarUnit, quantity.value);
|
|
1105
|
+
newYear = cal.year;
|
|
1106
|
+
newMonth = cal.month;
|
|
1107
|
+
newDay = cal.day;
|
|
1108
|
+
return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Time units: delegate to clock helper with precision preservation
|
|
1112
|
+
if (['hour', 'minute', 'second', 'millisecond'].includes(normalizedUnit)) {
|
|
1113
|
+
const clock = addClockParts(
|
|
1114
|
+
dt.hour,
|
|
1115
|
+
dt.minute,
|
|
1116
|
+
dt.second,
|
|
1117
|
+
dt.millisecond,
|
|
1118
|
+
normalizedUnit as ClockUnit,
|
|
1119
|
+
quantity.value,
|
|
1120
|
+
true
|
|
1121
|
+
);
|
|
1122
|
+
newHour = clock.hour;
|
|
1123
|
+
newMinute = clock.minute;
|
|
1124
|
+
newSecond = clock.second;
|
|
1125
|
+
newMillisecond = clock.millisecond;
|
|
1126
|
+
|
|
1127
|
+
if (clock.dayDelta !== 0) {
|
|
1128
|
+
const cal = addCalendarParts(newYear, newMonth, newDay, 'day', clock.dayDelta);
|
|
1129
|
+
newYear = cal.year;
|
|
1130
|
+
newMonth = cal.month;
|
|
1131
|
+
newDay = cal.day;
|
|
1132
|
+
}
|
|
1133
|
+
return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
// Component Extraction
|
|
1141
|
+
// ============================================================================
|
|
1142
|
+
|
|
1143
|
+
export function yearOf(temporal: TemporalValue): number | null {
|
|
1144
|
+
if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
|
|
1145
|
+
return temporal.year;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export function monthOf(temporal: TemporalValue): number | null {
|
|
1151
|
+
if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
|
|
1152
|
+
return temporal.month ?? null;
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function dayOf(temporal: TemporalValue): number | null {
|
|
1158
|
+
if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
|
|
1159
|
+
return temporal.day ?? null;
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function hourOf(temporal: TemporalValue): number | null {
|
|
1165
|
+
if (isFHIRTime(temporal)) {
|
|
1166
|
+
return temporal.hour;
|
|
1167
|
+
}
|
|
1168
|
+
if (isFHIRDateTime(temporal)) {
|
|
1169
|
+
return temporal.hour ?? null;
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export function minuteOf(temporal: TemporalValue): number | null {
|
|
1175
|
+
if (isFHIRTime(temporal)) {
|
|
1176
|
+
return temporal.minute ?? null;
|
|
1177
|
+
}
|
|
1178
|
+
if (isFHIRDateTime(temporal)) {
|
|
1179
|
+
return temporal.minute ?? null;
|
|
1180
|
+
}
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export function secondOf(temporal: TemporalValue): number | null {
|
|
1185
|
+
if (isFHIRTime(temporal)) {
|
|
1186
|
+
return temporal.second ?? null;
|
|
1187
|
+
}
|
|
1188
|
+
if (isFHIRDateTime(temporal)) {
|
|
1189
|
+
return temporal.second ?? null;
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export function millisecondOf(temporal: TemporalValue): number | null {
|
|
1195
|
+
if (isFHIRTime(temporal)) {
|
|
1196
|
+
return temporal.millisecond ?? null;
|
|
1197
|
+
}
|
|
1198
|
+
if (isFHIRDateTime(temporal)) {
|
|
1199
|
+
return temporal.millisecond ?? null;
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ============================================================================
|
|
1205
|
+
// Helper Functions
|
|
1206
|
+
// ============================================================================
|
|
1207
|
+
|
|
1208
|
+
// Shared helper scaffolding for refactor (TDD-first)
|
|
1209
|
+
export type CalendarUnit = 'year' | 'month' | 'week' | 'day';
|
|
1210
|
+
export type ClockUnit = 'hour' | 'minute' | 'second' | 'millisecond';
|
|
1211
|
+
|
|
1212
|
+
export interface CalendarPartsResult {
|
|
1213
|
+
year: number;
|
|
1214
|
+
month?: number;
|
|
1215
|
+
day?: number;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export interface ClockPartsResult {
|
|
1219
|
+
hour?: number;
|
|
1220
|
+
minute?: number;
|
|
1221
|
+
second?: number;
|
|
1222
|
+
millisecond?: number;
|
|
1223
|
+
// +1 when time addition crosses midnight forward, -1 when backward, 0 otherwise
|
|
1224
|
+
dayDelta: number;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Intentionally left unimplemented for now (tests drive implementation)
|
|
1228
|
+
export function addCalendarParts(
|
|
1229
|
+
year: number,
|
|
1230
|
+
month: number | undefined,
|
|
1231
|
+
day: number | undefined,
|
|
1232
|
+
unit: CalendarUnit,
|
|
1233
|
+
amount: number
|
|
1234
|
+
): CalendarPartsResult {
|
|
1235
|
+
const normalizedUnit = normalizeUnit(unit);
|
|
1236
|
+
|
|
1237
|
+
let newYear = year;
|
|
1238
|
+
let newMonth = month;
|
|
1239
|
+
let newDay = day;
|
|
1240
|
+
|
|
1241
|
+
if (normalizedUnit === 'week') {
|
|
1242
|
+
// Convert weeks to days using calendar semantics (truncate fractional weeks)
|
|
1243
|
+
const weeksToAdd = Math.trunc(amount);
|
|
1244
|
+
return addCalendarParts(newYear, newMonth, newDay, 'day', weeksToAdd * 7);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (normalizedUnit === 'year') {
|
|
1248
|
+
newYear += Math.trunc(amount);
|
|
1249
|
+
} else if (normalizedUnit === 'month') {
|
|
1250
|
+
const monthsToAdd = Math.trunc(amount);
|
|
1251
|
+
if (newMonth !== undefined) {
|
|
1252
|
+
let totalMonths = (newYear * 12) + (newMonth - 1) + monthsToAdd;
|
|
1253
|
+
newYear = Math.floor(totalMonths / 12);
|
|
1254
|
+
newMonth = (totalMonths % 12) + 1;
|
|
1255
|
+
if (newMonth <= 0) {
|
|
1256
|
+
newMonth += 12;
|
|
1257
|
+
newYear--;
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
// Coerce months into years when only year precision
|
|
1261
|
+
const yearsToAdd = convertAndTruncate(createTimeQuantity(amount, 'month'), 'year');
|
|
1262
|
+
newYear += yearsToAdd;
|
|
1263
|
+
}
|
|
1264
|
+
} else if (normalizedUnit === 'day') {
|
|
1265
|
+
const daysToAdd = Math.trunc(amount);
|
|
1266
|
+
if (newDay !== undefined && newMonth !== undefined) {
|
|
1267
|
+
// Proper calendar day arithmetic
|
|
1268
|
+
let currentYear = newYear;
|
|
1269
|
+
let currentMonth = newMonth;
|
|
1270
|
+
let currentDay = newDay + daysToAdd;
|
|
1271
|
+
|
|
1272
|
+
while (currentDay > getDaysInMonth(currentYear, currentMonth)) {
|
|
1273
|
+
currentDay -= getDaysInMonth(currentYear, currentMonth);
|
|
1274
|
+
currentMonth++;
|
|
1275
|
+
if (currentMonth > 12) {
|
|
1276
|
+
currentMonth = 1;
|
|
1277
|
+
currentYear++;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
while (currentDay < 1) {
|
|
1282
|
+
currentMonth--;
|
|
1283
|
+
if (currentMonth < 1) {
|
|
1284
|
+
currentMonth = 12;
|
|
1285
|
+
currentYear--;
|
|
1286
|
+
}
|
|
1287
|
+
currentDay += getDaysInMonth(currentYear, currentMonth);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
newYear = currentYear;
|
|
1291
|
+
newMonth = currentMonth;
|
|
1292
|
+
newDay = currentDay;
|
|
1293
|
+
} else if (newMonth !== undefined) {
|
|
1294
|
+
// Convert days to months when month precision but no day
|
|
1295
|
+
const monthsToAdd = convertAndTruncate(createTimeQuantity(amount, 'day'), 'month');
|
|
1296
|
+
let totalMonths = (newYear * 12) + (newMonth - 1) + monthsToAdd;
|
|
1297
|
+
newYear = Math.floor(totalMonths / 12);
|
|
1298
|
+
newMonth = (totalMonths % 12) + 1;
|
|
1299
|
+
if (newMonth <= 0) {
|
|
1300
|
+
newMonth += 12;
|
|
1301
|
+
newYear--;
|
|
1302
|
+
}
|
|
1303
|
+
} else {
|
|
1304
|
+
// Convert days to years when only year precision
|
|
1305
|
+
const yearsToAdd = convertAndTruncate(createTimeQuantity(amount, 'day'), 'year');
|
|
1306
|
+
newYear += yearsToAdd;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Clamp day for validity when we have a day and month
|
|
1311
|
+
newDay = clampDay(newYear, newMonth, newDay);
|
|
1312
|
+
|
|
1313
|
+
return { year: newYear, month: newMonth, day: newDay };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Intentionally left unimplemented for now (tests drive implementation)
|
|
1317
|
+
export function addClockParts(
|
|
1318
|
+
hour: number | undefined,
|
|
1319
|
+
minute: number | undefined,
|
|
1320
|
+
second: number | undefined,
|
|
1321
|
+
millisecond: number | undefined,
|
|
1322
|
+
unit: ClockUnit,
|
|
1323
|
+
amount: number,
|
|
1324
|
+
preservePrecision: boolean = true
|
|
1325
|
+
): ClockPartsResult {
|
|
1326
|
+
const HOUR_MS = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1327
|
+
const MINUTE_MS = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
|
|
1328
|
+
const SECOND_MS = MILLISECONDS_PER_SECOND;
|
|
1329
|
+
const DAY_MS = HOURS_PER_DAY * HOUR_MS;
|
|
1330
|
+
|
|
1331
|
+
// Build original total milliseconds from time-of-day
|
|
1332
|
+
let totalMs = (hour ?? 0) * HOUR_MS;
|
|
1333
|
+
if (minute !== undefined) totalMs += minute * MINUTE_MS;
|
|
1334
|
+
if (second !== undefined) totalMs += second * SECOND_MS;
|
|
1335
|
+
if (millisecond !== undefined) totalMs += millisecond;
|
|
1336
|
+
|
|
1337
|
+
// Compute quantity in milliseconds honoring truncation rules
|
|
1338
|
+
let deltaMs = 0;
|
|
1339
|
+
if (unit === 'hour') {
|
|
1340
|
+
deltaMs = Math.trunc(amount) * HOUR_MS;
|
|
1341
|
+
} else if (unit === 'minute') {
|
|
1342
|
+
// precision-preserving: we'll add minutes as milliseconds, but reconstruction preserves precision
|
|
1343
|
+
deltaMs = Math.trunc(amount) * MINUTE_MS;
|
|
1344
|
+
} else if (unit === 'second') {
|
|
1345
|
+
deltaMs = amount * SECOND_MS; // fractions become milliseconds
|
|
1346
|
+
} else if (unit === 'millisecond') {
|
|
1347
|
+
deltaMs = amount;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const newTotal = totalMs + deltaMs;
|
|
1351
|
+
// Compute dayDelta via floor division and get a non-negative wrapped remainder
|
|
1352
|
+
let dayDelta = Math.floor(newTotal / DAY_MS);
|
|
1353
|
+
let wrapped = newTotal - dayDelta * DAY_MS;
|
|
1354
|
+
|
|
1355
|
+
// Reconstruct time parts with precision preservation
|
|
1356
|
+
// Preserve hour precision: if hour was undefined, don't materialize it
|
|
1357
|
+
let newHour: number | undefined =
|
|
1358
|
+
hour !== undefined ? Math.floor(wrapped / HOUR_MS) : undefined;
|
|
1359
|
+
if (hour !== undefined) {
|
|
1360
|
+
wrapped %= HOUR_MS;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
let newMinute: number | undefined = undefined;
|
|
1364
|
+
let newSecond: number | undefined = undefined;
|
|
1365
|
+
let newMillisecond: number | undefined = undefined;
|
|
1366
|
+
|
|
1367
|
+
if (minute !== undefined) {
|
|
1368
|
+
newMinute = Math.floor(wrapped / MINUTE_MS);
|
|
1369
|
+
wrapped %= MINUTE_MS;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (second !== undefined) {
|
|
1373
|
+
newSecond = Math.floor(wrapped / SECOND_MS);
|
|
1374
|
+
wrapped %= SECOND_MS;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (millisecond !== undefined) {
|
|
1378
|
+
newMillisecond = Math.floor(wrapped);
|
|
1379
|
+
wrapped = 0;
|
|
1380
|
+
} else if (!preservePrecision && (second !== undefined || minute !== undefined)) {
|
|
1381
|
+
// Only when explicitly allowed to introduce finer precision
|
|
1382
|
+
newMillisecond = Math.floor(wrapped);
|
|
1383
|
+
wrapped = 0;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return {
|
|
1387
|
+
hour: newHour,
|
|
1388
|
+
minute: newMinute,
|
|
1389
|
+
second: newSecond,
|
|
1390
|
+
millisecond: newMillisecond,
|
|
1391
|
+
dayDelta
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Cache for UTC-normalized DateTimes to avoid repeated conversions
|
|
1396
|
+
const utcNormalizationCache = new WeakMap<FHIRDateTime, FHIRDateTime>();
|
|
1397
|
+
|
|
1398
|
+
function normalizeToUTC(dt: FHIRDateTime): FHIRDateTime {
|
|
1399
|
+
if (dt.timezoneOffset === undefined || dt.timezoneOffset === 0) {
|
|
1400
|
+
return dt;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Check cache first
|
|
1404
|
+
const cached = utcNormalizationCache.get(dt);
|
|
1405
|
+
if (cached) {
|
|
1406
|
+
return cached;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Convert to total minutes and adjust
|
|
1410
|
+
let totalMinutes = (dt.hour ?? 0) * 60 + (dt.minute ?? 0) - dt.timezoneOffset;
|
|
1411
|
+
|
|
1412
|
+
// Handle day boundary crossing
|
|
1413
|
+
let dayAdjust = 0;
|
|
1414
|
+
if (totalMinutes < 0) {
|
|
1415
|
+
dayAdjust = -Math.ceil(Math.abs(totalMinutes) / (24 * 60));
|
|
1416
|
+
totalMinutes += Math.abs(dayAdjust) * 24 * 60;
|
|
1417
|
+
} else if (totalMinutes >= 24 * 60) {
|
|
1418
|
+
dayAdjust = Math.floor(totalMinutes / (24 * 60));
|
|
1419
|
+
totalMinutes %= 24 * 60;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const newHour = Math.floor(totalMinutes / 60);
|
|
1423
|
+
const newMinute = totalMinutes % 60;
|
|
1424
|
+
|
|
1425
|
+
// Adjust day if needed
|
|
1426
|
+
let newDay = dt.day;
|
|
1427
|
+
let newMonth = dt.month;
|
|
1428
|
+
let newYear = dt.year;
|
|
1429
|
+
|
|
1430
|
+
if (dayAdjust !== 0 && newDay !== undefined && newMonth !== undefined) {
|
|
1431
|
+
// Use proper calendar arithmetic for day adjustment
|
|
1432
|
+
const tempDate = createDate(newYear, newMonth, newDay);
|
|
1433
|
+
const dayQuantity = createTimeQuantity(dayAdjust, 'day');
|
|
1434
|
+
const adjustedDate = addToDate(tempDate, dayQuantity);
|
|
1435
|
+
newYear = adjustedDate.year;
|
|
1436
|
+
newMonth = adjustedDate.month;
|
|
1437
|
+
newDay = adjustedDate.day;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const result: FHIRDateTime = {
|
|
1441
|
+
kind: 'FHIRDateTime',
|
|
1442
|
+
year: newYear,
|
|
1443
|
+
month: newMonth,
|
|
1444
|
+
day: newDay,
|
|
1445
|
+
hour: dt.hour !== undefined ? newHour : undefined,
|
|
1446
|
+
minute: dt.minute !== undefined ? newMinute : undefined,
|
|
1447
|
+
second: dt.second,
|
|
1448
|
+
millisecond: dt.millisecond,
|
|
1449
|
+
timezoneOffset: 0,
|
|
1450
|
+
precision: dt.precision
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
// Store in cache for future lookups
|
|
1454
|
+
utcNormalizationCache.set(dt, result);
|
|
1455
|
+
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ============================================================================
|
|
1460
|
+
// Backwards Compatibility Exports (temporary during migration)
|
|
1461
|
+
// ============================================================================
|
|
1462
|
+
|
|
1463
|
+
// Export classes that map to factory functions
|
|
1464
|
+
export const FHIRDate = {
|
|
1465
|
+
new: createDate,
|
|
1466
|
+
create: createDate
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
export const FHIRTime = {
|
|
1470
|
+
new: createTime,
|
|
1471
|
+
create: createTime
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
export const FHIRDateTime = {
|
|
1475
|
+
new: createDateTime,
|
|
1476
|
+
create: createDateTime
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
// ============================================================================
|
|
1480
|
+
// Boundary Functions
|
|
1481
|
+
// ============================================================================
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Calculate the low boundary for a Date value
|
|
1485
|
+
*/
|
|
1486
|
+
export function getDateLowBoundary(date: FHIRDate, precision?: number): FHIRDate | null {
|
|
1487
|
+
// Validate precision
|
|
1488
|
+
if (precision !== undefined) {
|
|
1489
|
+
if (precision < 0 || precision > 8) {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
// Default precision for Date is 8 (day)
|
|
1494
|
+
precision = 8;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Build the boundary date based on precision
|
|
1498
|
+
let year = date.year;
|
|
1499
|
+
let month: number | undefined;
|
|
1500
|
+
let day: number | undefined;
|
|
1501
|
+
|
|
1502
|
+
if (precision >= 6) {
|
|
1503
|
+
month = date.month ?? 1;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (precision >= 8) {
|
|
1507
|
+
day = date.day ?? 1;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
return createDate(year, month, day);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Calculate the high boundary for a Date value
|
|
1515
|
+
*/
|
|
1516
|
+
export function getDateHighBoundary(date: FHIRDate, precision?: number): FHIRDate | null {
|
|
1517
|
+
// Validate precision
|
|
1518
|
+
if (precision !== undefined) {
|
|
1519
|
+
if (precision < 0 || precision > 8) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
} else {
|
|
1523
|
+
// Default precision for Date is 8 (day)
|
|
1524
|
+
precision = 8;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Build the boundary date based on precision
|
|
1528
|
+
let year = date.year;
|
|
1529
|
+
let month: number | undefined;
|
|
1530
|
+
let day: number | undefined;
|
|
1531
|
+
|
|
1532
|
+
if (precision >= 6) {
|
|
1533
|
+
month = date.month ?? 12;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (precision >= 8) {
|
|
1537
|
+
if (date.day !== undefined) {
|
|
1538
|
+
day = date.day;
|
|
1539
|
+
} else {
|
|
1540
|
+
// Need to calculate last day of month
|
|
1541
|
+
const actualMonth = month ?? 12;
|
|
1542
|
+
day = getDaysInMonth(year, actualMonth);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return createDate(year, month, day);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Calculate the low boundary for a DateTime value
|
|
1551
|
+
*/
|
|
1552
|
+
export function getDateTimeLowBoundary(dateTime: FHIRDateTime, precision?: number): FHIRDateTime | null {
|
|
1553
|
+
// Validate precision
|
|
1554
|
+
if (precision !== undefined) {
|
|
1555
|
+
if (precision < 0 || precision > 17) {
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
// Default precision for DateTime is 17 (millisecond)
|
|
1560
|
+
precision = 17;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Build the boundary datetime based on precision
|
|
1564
|
+
let year = dateTime.year;
|
|
1565
|
+
let month: number | undefined;
|
|
1566
|
+
let day: number | undefined;
|
|
1567
|
+
let hour: number | undefined;
|
|
1568
|
+
let minute: number | undefined;
|
|
1569
|
+
let second: number | undefined;
|
|
1570
|
+
let millisecond: number | undefined;
|
|
1571
|
+
let timezoneOffset: number | undefined = dateTime.timezoneOffset;
|
|
1572
|
+
|
|
1573
|
+
if (precision >= 6) {
|
|
1574
|
+
month = dateTime.month ?? 1;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (precision >= 8) {
|
|
1578
|
+
day = dateTime.day ?? 1;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (precision >= 10) {
|
|
1582
|
+
hour = dateTime.hour ?? 0;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (precision >= 12) {
|
|
1586
|
+
minute = dateTime.minute ?? 0;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (precision >= 14) {
|
|
1590
|
+
second = dateTime.second ?? 0;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (precision >= 17) {
|
|
1594
|
+
millisecond = dateTime.millisecond ?? 0;
|
|
1595
|
+
|
|
1596
|
+
// If no timezone was specified and we're at millisecond precision,
|
|
1597
|
+
// use the maximum positive offset (+14:00 = 840 minutes)
|
|
1598
|
+
if (timezoneOffset === undefined && dateTime.hour !== undefined) {
|
|
1599
|
+
timezoneOffset = 840; // +14:00
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Calculate the high boundary for a DateTime value
|
|
1608
|
+
*/
|
|
1609
|
+
export function getDateTimeHighBoundary(dateTime: FHIRDateTime, precision?: number): FHIRDateTime | null {
|
|
1610
|
+
// Validate precision
|
|
1611
|
+
if (precision !== undefined) {
|
|
1612
|
+
if (precision < 0 || precision > 17) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
} else {
|
|
1616
|
+
// Default precision for DateTime is 17 (millisecond)
|
|
1617
|
+
precision = 17;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Build the boundary datetime based on precision
|
|
1621
|
+
let year = dateTime.year;
|
|
1622
|
+
let month: number | undefined;
|
|
1623
|
+
let day: number | undefined;
|
|
1624
|
+
let hour: number | undefined;
|
|
1625
|
+
let minute: number | undefined;
|
|
1626
|
+
let second: number | undefined;
|
|
1627
|
+
let millisecond: number | undefined;
|
|
1628
|
+
let timezoneOffset: number | undefined = dateTime.timezoneOffset;
|
|
1629
|
+
|
|
1630
|
+
if (precision >= 6) {
|
|
1631
|
+
month = dateTime.month ?? 12;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (precision >= 8) {
|
|
1635
|
+
if (dateTime.day !== undefined) {
|
|
1636
|
+
day = dateTime.day;
|
|
1637
|
+
} else {
|
|
1638
|
+
// Need to calculate last day of month
|
|
1639
|
+
const actualMonth = month ?? 12;
|
|
1640
|
+
day = getDaysInMonth(year, actualMonth);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (precision >= 10) {
|
|
1645
|
+
hour = dateTime.hour ?? 23;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (precision >= 12) {
|
|
1649
|
+
minute = dateTime.minute ?? 59;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (precision >= 14) {
|
|
1653
|
+
second = dateTime.second ?? 59;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (precision >= 17) {
|
|
1657
|
+
millisecond = dateTime.millisecond ?? 999;
|
|
1658
|
+
|
|
1659
|
+
// If no timezone was specified and we're at millisecond precision,
|
|
1660
|
+
// use the maximum negative offset (-12:00 = -720 minutes)
|
|
1661
|
+
if (timezoneOffset === undefined && dateTime.hour !== undefined) {
|
|
1662
|
+
timezoneOffset = -720; // -12:00
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Calculate the low boundary for a Time value
|
|
1671
|
+
*/
|
|
1672
|
+
export function getTimeLowBoundary(time: FHIRTime, precision?: number): FHIRTime | null {
|
|
1673
|
+
// Validate precision
|
|
1674
|
+
if (precision !== undefined) {
|
|
1675
|
+
if (precision < 0 || precision > 9) {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
// Default precision for Time is 9 (millisecond)
|
|
1680
|
+
precision = 9;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Build the boundary time based on precision
|
|
1684
|
+
let hour = time.hour;
|
|
1685
|
+
let minute: number | undefined;
|
|
1686
|
+
let second: number | undefined;
|
|
1687
|
+
let millisecond: number | undefined;
|
|
1688
|
+
|
|
1689
|
+
if (precision >= 5) {
|
|
1690
|
+
minute = time.minute ?? 0;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (precision >= 7) {
|
|
1694
|
+
second = time.second ?? 0;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
if (precision >= 9) {
|
|
1698
|
+
millisecond = time.millisecond ?? 0;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
return createTime(hour, minute, second, millisecond);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Calculate the high boundary for a Time value
|
|
1706
|
+
*/
|
|
1707
|
+
export function getTimeHighBoundary(time: FHIRTime, precision?: number): FHIRTime | null {
|
|
1708
|
+
// Validate precision
|
|
1709
|
+
if (precision !== undefined) {
|
|
1710
|
+
if (precision < 0 || precision > 9) {
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
} else {
|
|
1714
|
+
// Default precision for Time is 9 (millisecond)
|
|
1715
|
+
precision = 9;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Build the boundary time based on precision
|
|
1719
|
+
let hour = time.hour;
|
|
1720
|
+
let minute: number | undefined;
|
|
1721
|
+
let second: number | undefined;
|
|
1722
|
+
let millisecond: number | undefined;
|
|
1723
|
+
|
|
1724
|
+
if (precision >= 5) {
|
|
1725
|
+
minute = time.minute ?? 59;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (precision >= 7) {
|
|
1729
|
+
second = time.second ?? 59;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
if (precision >= 9) {
|
|
1733
|
+
millisecond = time.millisecond ?? 999;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
return createTime(hour, minute, second, millisecond);
|
|
1737
|
+
}
|