@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.
Files changed (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. 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
+ }