@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900

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 (147) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +226 -120
  3. package/dist/index.js +11552 -5580
  4. package/dist/index.js.map +1 -1
  5. package/package.json +12 -5
  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 +939 -1204
  13. package/src/completion-provider.ts +209 -191
  14. package/src/complex-types/quantity-value.ts +410 -0
  15. package/src/complex-types/temporal.ts +1776 -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 +506 -468
  23. package/src/lexer.ts +192 -211
  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 +99 -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 +744 -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 +132 -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/extension-function.ts +84 -0
  64. package/src/operations/first-function.ts +1 -1
  65. package/src/operations/floor-function.ts +1 -1
  66. package/src/operations/greater-operator.ts +7 -9
  67. package/src/operations/greater-or-equal-operator.ts +7 -9
  68. package/src/operations/highBoundary-function.ts +120 -0
  69. package/src/operations/hourOf-function.ts +66 -0
  70. package/src/operations/iif-function.ts +193 -8
  71. package/src/operations/implies-operator.ts +2 -1
  72. package/src/operations/in-operator.ts +2 -1
  73. package/src/operations/index.ts +43 -0
  74. package/src/operations/indexOf-function.ts +1 -1
  75. package/src/operations/intersect-function.ts +1 -1
  76. package/src/operations/is-function.ts +70 -0
  77. package/src/operations/is-operator.ts +176 -13
  78. package/src/operations/isDistinct-function.ts +2 -1
  79. package/src/operations/join-function.ts +1 -1
  80. package/src/operations/last-function.ts +1 -1
  81. package/src/operations/lastIndexOf-function.ts +85 -0
  82. package/src/operations/length-function.ts +1 -1
  83. package/src/operations/less-operator.ts +8 -9
  84. package/src/operations/less-or-equal-operator.ts +7 -9
  85. package/src/operations/less-than.ts +8 -13
  86. package/src/operations/lowBoundary-function.ts +120 -0
  87. package/src/operations/lower-function.ts +1 -1
  88. package/src/operations/matches-function.ts +86 -0
  89. package/src/operations/matchesFull-function.ts +96 -0
  90. package/src/operations/millisecondOf-function.ts +66 -0
  91. package/src/operations/minus-operator.ts +76 -4
  92. package/src/operations/minuteOf-function.ts +66 -0
  93. package/src/operations/mod-operator.ts +8 -2
  94. package/src/operations/monthOf-function.ts +66 -0
  95. package/src/operations/multiply-operator.ts +27 -3
  96. package/src/operations/not-equal-operator.ts +24 -30
  97. package/src/operations/not-equivalent-operator.ts +13 -53
  98. package/src/operations/not-function.ts +10 -3
  99. package/src/operations/ofType-function.ts +43 -12
  100. package/src/operations/or-operator.ts +2 -1
  101. package/src/operations/plus-operator.ts +71 -7
  102. package/src/operations/power-function.ts +35 -10
  103. package/src/operations/precision-function.ts +146 -0
  104. package/src/operations/repeat-function.ts +169 -0
  105. package/src/operations/replace-function.ts +1 -1
  106. package/src/operations/replaceMatches-function.ts +125 -0
  107. package/src/operations/round-function.ts +1 -1
  108. package/src/operations/secondOf-function.ts +66 -0
  109. package/src/operations/select-function.ts +66 -5
  110. package/src/operations/single-function.ts +1 -1
  111. package/src/operations/skip-function.ts +1 -1
  112. package/src/operations/split-function.ts +1 -1
  113. package/src/operations/sqrt-function.ts +15 -8
  114. package/src/operations/startsWith-function.ts +1 -1
  115. package/src/operations/subsetOf-function.ts +6 -2
  116. package/src/operations/substring-function.ts +1 -1
  117. package/src/operations/supersetOf-function.ts +6 -2
  118. package/src/operations/tail-function.ts +1 -1
  119. package/src/operations/take-function.ts +1 -1
  120. package/src/operations/temporal-functions.ts +555 -0
  121. package/src/operations/timeOf-function.ts +67 -0
  122. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  123. package/src/operations/toBoolean-function.ts +27 -8
  124. package/src/operations/toChars-function.ts +56 -0
  125. package/src/operations/toDecimal-function.ts +27 -8
  126. package/src/operations/toInteger-function.ts +15 -3
  127. package/src/operations/toLong-function.ts +98 -0
  128. package/src/operations/toQuantity-function.ts +181 -0
  129. package/src/operations/toString-function.ts +78 -15
  130. package/src/operations/trace-function.ts +1 -1
  131. package/src/operations/trim-function.ts +1 -1
  132. package/src/operations/truncate-function.ts +1 -1
  133. package/src/operations/unary-minus-operator.ts +2 -2
  134. package/src/operations/unary-plus-operator.ts +1 -1
  135. package/src/operations/union-function.ts +1 -1
  136. package/src/operations/union-operator.ts +16 -26
  137. package/src/operations/upper-function.ts +1 -1
  138. package/src/operations/where-function.ts +3 -3
  139. package/src/operations/xor-operator.ts +1 -1
  140. package/src/operations/yearOf-function.ts +66 -0
  141. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  142. package/src/parser.ts +262 -503
  143. package/src/registry.ts +53 -42
  144. package/src/types.ts +129 -17
  145. package/src/utils/decimal.ts +76 -0
  146. package/src/utils/pprint.ts +151 -0
  147. package/src/quantity-value.ts +0 -198
@@ -0,0 +1,1776 @@
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
+ // Special case: DateTime and Date can be compared when date portions differ
465
+ if (isFHIRDateTime(a) && isFHIRDate(b)) {
466
+ // Compare the date portion of DateTime with Date
467
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
468
+ if (b.month !== undefined && a.month !== undefined) {
469
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
470
+ }
471
+ if (b.day !== undefined && a.day !== undefined) {
472
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
473
+ }
474
+ // When date portions are equal, Date and DateTime are incomparable
475
+ return null;
476
+ }
477
+ if (isFHIRDate(a) && isFHIRDateTime(b)) {
478
+ // Compare Date with the date portion of DateTime
479
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
480
+ if (a.month !== undefined && b.month !== undefined) {
481
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
482
+ }
483
+ if (a.day !== undefined && b.day !== undefined) {
484
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
485
+ }
486
+ // When date portions are equal, Date and DateTime are incomparable
487
+ return null;
488
+ }
489
+
490
+ // Different types (except DateTime/Date) can't be compared
491
+ if (a.kind !== b.kind) {
492
+ return null;
493
+ }
494
+
495
+ if (isFHIRDate(a) && isFHIRDate(b)) {
496
+ // Compare year (always present)
497
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
498
+
499
+ // Compare month - if one has it and other doesn't, return null
500
+ if (a.month !== undefined || b.month !== undefined) {
501
+ if (a.month === undefined || b.month === undefined) return null;
502
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
503
+ }
504
+
505
+ // Compare day - if one has it and other doesn't, return null
506
+ if (a.day !== undefined || b.day !== undefined) {
507
+ if (a.day === undefined || b.day === undefined) return null;
508
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
509
+ }
510
+
511
+ return 0;
512
+ }
513
+
514
+ if (isFHIRTime(a) && isFHIRTime(b)) {
515
+ // Compare hour (always present)
516
+ if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1;
517
+
518
+ // Compare minute - if one has it and other doesn't, return null
519
+ if (a.minute !== undefined || b.minute !== undefined) {
520
+ if (a.minute === undefined || b.minute === undefined) return null;
521
+ if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1;
522
+ }
523
+
524
+ // Compare seconds+milliseconds as a single precision group
525
+ if ((a.second !== undefined || a.millisecond !== undefined) ||
526
+ (b.second !== undefined || b.millisecond !== undefined)) {
527
+ const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
528
+ const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
529
+ if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
530
+ const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
531
+ const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
532
+ if (aMs !== bMs) return aMs < bMs ? -1 : 1;
533
+ }
534
+
535
+ return 0;
536
+ }
537
+
538
+ if (isFHIRDateTime(a) && isFHIRDateTime(b)) {
539
+ // Handle timezone comparison
540
+ if (a.timezoneOffset !== undefined && b.timezoneOffset !== undefined) {
541
+ // Both have timezones - normalize to UTC
542
+ const aUtc = normalizeToUTC(a);
543
+ const bUtc = normalizeToUTC(b);
544
+
545
+ // Compare year (always present)
546
+ if (aUtc.year !== bUtc.year) return aUtc.year < bUtc.year ? -1 : 1;
547
+
548
+ // Compare month
549
+ if (aUtc.month !== undefined || bUtc.month !== undefined) {
550
+ if (aUtc.month === undefined || bUtc.month === undefined) return null;
551
+ if (aUtc.month !== bUtc.month) return aUtc.month < bUtc.month ? -1 : 1;
552
+ }
553
+
554
+ // Compare day
555
+ if (aUtc.day !== undefined || bUtc.day !== undefined) {
556
+ if (aUtc.day === undefined || bUtc.day === undefined) return null;
557
+ if (aUtc.day !== bUtc.day) return aUtc.day < bUtc.day ? -1 : 1;
558
+ }
559
+
560
+ // Compare hour
561
+ if (aUtc.hour !== undefined || bUtc.hour !== undefined) {
562
+ if (aUtc.hour === undefined || bUtc.hour === undefined) return null;
563
+ if (aUtc.hour !== bUtc.hour) return aUtc.hour < bUtc.hour ? -1 : 1;
564
+ }
565
+
566
+ // Compare minute
567
+ if (aUtc.minute !== undefined || bUtc.minute !== undefined) {
568
+ if (aUtc.minute === undefined || bUtc.minute === undefined) return null;
569
+ if (aUtc.minute !== bUtc.minute) return aUtc.minute < bUtc.minute ? -1 : 1;
570
+ }
571
+
572
+ // Compare seconds+milliseconds as a single precision group
573
+ if ((aUtc.second !== undefined || aUtc.millisecond !== undefined) ||
574
+ (bUtc.second !== undefined || bUtc.millisecond !== undefined)) {
575
+ const aHasSecondPrecision = aUtc.second !== undefined || aUtc.millisecond !== undefined;
576
+ const bHasSecondPrecision = bUtc.second !== undefined || bUtc.millisecond !== undefined;
577
+ if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
578
+ const aMs = (aUtc.second ?? 0) * 1000 + (aUtc.millisecond ?? 0);
579
+ const bMs = (bUtc.second ?? 0) * 1000 + (bUtc.millisecond ?? 0);
580
+ if (aMs !== bMs) return aMs < bMs ? -1 : 1;
581
+ }
582
+
583
+ return 0;
584
+ } else if (a.timezoneOffset === undefined && b.timezoneOffset === undefined) {
585
+ // Both naive - direct comparison
586
+ // Compare year (always present)
587
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
588
+
589
+ // Compare month
590
+ if (a.month !== undefined || b.month !== undefined) {
591
+ if (a.month === undefined || b.month === undefined) return null;
592
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
593
+ }
594
+
595
+ // Compare day
596
+ if (a.day !== undefined || b.day !== undefined) {
597
+ if (a.day === undefined || b.day === undefined) return null;
598
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
599
+ }
600
+
601
+ // Compare hour
602
+ if (a.hour !== undefined || b.hour !== undefined) {
603
+ if (a.hour === undefined || b.hour === undefined) return null;
604
+ if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1;
605
+ }
606
+
607
+ // Compare minute
608
+ if (a.minute !== undefined || b.minute !== undefined) {
609
+ if (a.minute === undefined || b.minute === undefined) return null;
610
+ if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1;
611
+ }
612
+
613
+ // Compare seconds+milliseconds as a single precision group
614
+ if ((a.second !== undefined || a.millisecond !== undefined) ||
615
+ (b.second !== undefined || b.millisecond !== undefined)) {
616
+ const aHasSecondPrecision = a.second !== undefined || a.millisecond !== undefined;
617
+ const bHasSecondPrecision = b.second !== undefined || b.millisecond !== undefined;
618
+ if (!aHasSecondPrecision || !bHasSecondPrecision) return null;
619
+ const aMs = (a.second ?? 0) * 1000 + (a.millisecond ?? 0);
620
+ const bMs = (b.second ?? 0) * 1000 + (b.millisecond ?? 0);
621
+ if (aMs !== bMs) return aMs < bMs ? -1 : 1;
622
+ }
623
+
624
+ return 0;
625
+ } else {
626
+ // Mixed timezone state - can't compare
627
+ return null;
628
+ }
629
+ }
630
+
631
+ return null;
632
+ }
633
+
634
+ // ============================================================================
635
+ // String Formatting
636
+ // ============================================================================
637
+
638
+ export function toTemporalString(value: TemporalValue): string {
639
+ if (isFHIRDate(value)) {
640
+ return formatDate(value);
641
+ }
642
+ if (isFHIRTime(value)) {
643
+ return formatTime(value);
644
+ }
645
+ if (isFHIRDateTime(value)) {
646
+ return formatDateTime(value);
647
+ }
648
+ return '';
649
+ }
650
+
651
+ export function toFHIRPathLiteral(value: TemporalValue): string {
652
+ if (isFHIRDate(value)) {
653
+ return `@${formatDate(value)}`;
654
+ }
655
+ if (isFHIRTime(value)) {
656
+ return `@T${formatTime(value)}`;
657
+ }
658
+ if (isFHIRDateTime(value)) {
659
+ return `@${formatDateTime(value)}`;
660
+ }
661
+ return '';
662
+ }
663
+
664
+ function formatDate(date: FHIRDate): string {
665
+ const yearStr = date.year >= 0 && date.year < 10000 ?
666
+ String(date.year).padStart(4, '0') :
667
+ String(date.year);
668
+
669
+ if (date.precision.level === 'year') {
670
+ return yearStr;
671
+ } else if (date.precision.level === 'month') {
672
+ return `${yearStr}-${String(date.month).padStart(2, '0')}`;
673
+ } else {
674
+ return `${yearStr}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
675
+ }
676
+ }
677
+
678
+ function formatTime(time: FHIRTime): string {
679
+ let result = String(time.hour).padStart(2, '0');
680
+
681
+ if (time.minute !== undefined) {
682
+ result += ':' + String(time.minute).padStart(2, '0');
683
+
684
+ if (time.second !== undefined) {
685
+ result += ':' + String(time.second).padStart(2, '0');
686
+
687
+ if (time.millisecond !== undefined) {
688
+ result += '.' + String(time.millisecond).padStart(3, '0');
689
+ }
690
+ }
691
+ }
692
+
693
+ return result;
694
+ }
695
+
696
+ function formatDateTime(dt: FHIRDateTime): string {
697
+ const yearStr = dt.year >= 0 && dt.year < 10000 ?
698
+ String(dt.year).padStart(4, '0') :
699
+ String(dt.year);
700
+
701
+ let result = yearStr;
702
+
703
+ if (dt.month !== undefined) {
704
+ result += '-' + String(dt.month).padStart(2, '0');
705
+
706
+ if (dt.day !== undefined) {
707
+ result += '-' + String(dt.day).padStart(2, '0');
708
+
709
+ if (dt.hour !== undefined) {
710
+ result += 'T' + String(dt.hour).padStart(2, '0');
711
+
712
+ if (dt.minute !== undefined) {
713
+ result += ':' + String(dt.minute).padStart(2, '0');
714
+
715
+ if (dt.second !== undefined) {
716
+ result += ':' + String(dt.second).padStart(2, '0');
717
+
718
+ if (dt.millisecond !== undefined) {
719
+ result += '.' + String(dt.millisecond).padStart(3, '0');
720
+ }
721
+ }
722
+ }
723
+
724
+ // Add timezone
725
+ if (dt.timezoneOffset !== undefined) {
726
+ if (dt.timezoneOffset === 0) {
727
+ result += 'Z';
728
+ } else {
729
+ const sign = dt.timezoneOffset < 0 ? '-' : '+';
730
+ const absOffset = Math.abs(dt.timezoneOffset);
731
+ const hours = Math.floor(absOffset / 60);
732
+ const minutes = absOffset % 60;
733
+ result += sign + String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0');
734
+ }
735
+ }
736
+ }
737
+ // No T suffix when we have complete date (year-month-day) but no time
738
+ } else {
739
+ // Month precision with T suffix (partial date)
740
+ result += 'T';
741
+ }
742
+ } else {
743
+ // Year precision with T suffix (partial date)
744
+ result += 'T';
745
+ }
746
+
747
+ return result;
748
+ }
749
+
750
+ // ============================================================================
751
+ // Parsing
752
+ // ============================================================================
753
+
754
+ export function parseTemporalLiteral(literal: string): TemporalValue {
755
+ if (!literal.startsWith('@')) {
756
+ throw new Error('Temporal literal must start with @');
757
+ }
758
+
759
+ const value = literal.substring(1);
760
+
761
+ // Time literal: @T...
762
+ if (value.startsWith('T')) {
763
+ return parseTimeLiteral(value.substring(1));
764
+ }
765
+
766
+ // Check for DateTime vs Date
767
+ const hasTimeComponent = /T\d/.test(value);
768
+
769
+ if (hasTimeComponent) {
770
+ return parseDateTimeLiteral(value);
771
+ } else if (value.endsWith('T')) {
772
+ // DateTime with only date precision
773
+ return parseDateTimeLiteral(value.slice(0, -1));
774
+ } else {
775
+ // Date literal
776
+ return parseDateLiteral(value);
777
+ }
778
+ }
779
+
780
+ function parseDateLiteral(value: string): FHIRDate {
781
+ const match = value.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/);
782
+ if (!match) {
783
+ throw new Error(`Invalid date literal: @${value}`);
784
+ }
785
+
786
+ const year = parseInt(match[1]!, 10);
787
+ const month = match[2] ? parseInt(match[2]!, 10) : undefined;
788
+ const day = match[3] ? parseInt(match[3]!, 10) : undefined;
789
+
790
+ return createDate(year, month, day);
791
+ }
792
+
793
+ function parseTimeLiteral(value: string): FHIRTime {
794
+ const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
795
+ if (!match) {
796
+ throw new Error(`Invalid time literal: @T${value}`);
797
+ }
798
+
799
+ const hour = parseInt(match[1]!, 10);
800
+ const minute = match[2] ? parseInt(match[2]!, 10) : undefined;
801
+ const second = match[3] ? parseInt(match[3]!, 10) : undefined;
802
+ // Handle variable-length fractional seconds (pad or truncate to 3 digits)
803
+ let millisecond: number | undefined;
804
+ if (match[4]) {
805
+ const fraction = match[4];
806
+ // Pad to 3 digits if needed, truncate if longer
807
+ const padded = (fraction + '000').substring(0, 3);
808
+ millisecond = parseInt(padded, 10);
809
+ }
810
+
811
+ return createTime(hour, minute, second, millisecond);
812
+ }
813
+
814
+ function parseDateTimeLiteral(value: string): FHIRDateTime {
815
+ // Remove trailing T if present (for date-only DateTime)
816
+ const cleanValue = value.endsWith('T') ? value.slice(0, -1) : value;
817
+
818
+ // Split into date and time parts
819
+ const parts = cleanValue.split('T');
820
+ const datePart = parts[0];
821
+ const timePart = parts[1];
822
+
823
+ // Parse date part
824
+ const dateMatch = datePart?.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/);
825
+ if (!dateMatch) {
826
+ throw new Error(`Invalid datetime literal: @${value}`);
827
+ }
828
+
829
+ const year = parseInt(dateMatch[1]!, 10);
830
+ const month = dateMatch[2] ? parseInt(dateMatch[2]!, 10) : undefined;
831
+ const day = dateMatch[3] ? parseInt(dateMatch[3]!, 10) : undefined;
832
+
833
+ let hour: number | undefined;
834
+ let minute: number | undefined;
835
+ let second: number | undefined;
836
+ let millisecond: number | undefined;
837
+ let timezoneOffset: number | undefined;
838
+
839
+ // Parse time part if present
840
+ if (timePart) {
841
+ // Extract timezone
842
+ let timeWithoutTz = timePart;
843
+ const tzMatch = timePart.match(/(Z|[+-]\d{2}:\d{2})$/);
844
+
845
+ if (tzMatch) {
846
+ timeWithoutTz = timePart.substring(0, timePart.length - tzMatch[0].length);
847
+ const tz = tzMatch[0];
848
+
849
+ if (tz === 'Z') {
850
+ timezoneOffset = 0;
851
+ } else {
852
+ const tzParts = tz.match(/([+-])(\d{2}):(\d{2})/);
853
+ if (tzParts) {
854
+ const sign = tzParts[1] === '+' ? 1 : -1;
855
+ const hours = parseInt(tzParts[2]!, 10);
856
+ const minutes = parseInt(tzParts[3]!, 10);
857
+ timezoneOffset = sign * (hours * 60 + minutes);
858
+ }
859
+ }
860
+ }
861
+
862
+ // Parse time components - handle variable-length milliseconds
863
+ const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
864
+ if (timeMatch) {
865
+ hour = parseInt(timeMatch[1]!, 10);
866
+ minute = timeMatch[2] ? parseInt(timeMatch[2]!, 10) : undefined;
867
+ second = timeMatch[3] ? parseInt(timeMatch[3]!, 10) : undefined;
868
+ // Handle variable-length fractional seconds (pad or truncate to 3 digits)
869
+ if (timeMatch[4]) {
870
+ const fraction = timeMatch[4];
871
+ // Pad to 3 digits if needed, truncate if longer
872
+ const padded = (fraction + '000').substring(0, 3);
873
+ millisecond = parseInt(padded, 10);
874
+ }
875
+ }
876
+ }
877
+
878
+ return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
879
+ }
880
+
881
+ // ============================================================================
882
+ // Arithmetic Operations
883
+ // ============================================================================
884
+
885
+ const DAYS_PER_MONTH = 30;
886
+ const DAYS_PER_YEAR = 365;
887
+ const DAYS_PER_WEEK = 7;
888
+ const HOURS_PER_DAY = 24;
889
+ const MINUTES_PER_HOUR = 60;
890
+ const SECONDS_PER_MINUTE = 60;
891
+ const MILLISECONDS_PER_SECOND = 1000;
892
+
893
+ function normalizeUnit(unit: string): string {
894
+ // Handle UCUM units
895
+ const ucumMap: Record<string, string> = {
896
+ // 'a' (annum) is not supported for temporal arithmetic - use 'year' keyword
897
+ 'mo': 'month', // month
898
+ 'wk': 'week', // week
899
+ 'd': 'day', // day
900
+ 'h': 'hour', // hour
901
+ 'min': 'minute', // minute
902
+ 's': 'second', // second
903
+ 'ms': 'millisecond' // millisecond
904
+ };
905
+
906
+ if (ucumMap[unit]) {
907
+ return ucumMap[unit];
908
+ }
909
+
910
+ // Remove plurals for regular units
911
+ if (unit.endsWith('s') && unit !== 's' && unit !== 'ms') {
912
+ return unit.slice(0, -1);
913
+ }
914
+
915
+ return unit;
916
+ }
917
+
918
+ function convertAndTruncate(quantity: TimeQuantity, targetUnit: string): number {
919
+ const normalizedQuantityUnit = normalizeUnit(quantity.unit);
920
+
921
+ // Direct conversion for calendar units (year <-> month)
922
+ if (normalizedQuantityUnit === 'month' && targetUnit === 'year') {
923
+ return Math.trunc(quantity.value / 12);
924
+ }
925
+ if (normalizedQuantityUnit === 'year' && targetUnit === 'month') {
926
+ return Math.trunc(quantity.value * 12);
927
+ }
928
+
929
+ let valueInDays = 0;
930
+
931
+ // Convert to days first
932
+ switch (normalizedQuantityUnit) {
933
+ case 'year':
934
+ valueInDays = quantity.value * DAYS_PER_YEAR;
935
+ break;
936
+ case 'month':
937
+ valueInDays = quantity.value * DAYS_PER_MONTH;
938
+ break;
939
+ case 'week':
940
+ valueInDays = quantity.value * DAYS_PER_WEEK;
941
+ break;
942
+ case 'day':
943
+ valueInDays = quantity.value;
944
+ break;
945
+ case 'hour':
946
+ valueInDays = quantity.value / HOURS_PER_DAY;
947
+ break;
948
+ case 'minute':
949
+ valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR);
950
+ break;
951
+ case 'second':
952
+ valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
953
+ break;
954
+ case 'millisecond':
955
+ valueInDays = quantity.value / (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND);
956
+ break;
957
+ }
958
+
959
+ // Convert from days to target unit - use Math.trunc for truncation towards zero
960
+ switch (targetUnit) {
961
+ case 'year':
962
+ return Math.trunc(valueInDays / DAYS_PER_YEAR);
963
+ case 'month':
964
+ return Math.trunc(valueInDays / DAYS_PER_MONTH);
965
+ case 'day':
966
+ return Math.trunc(valueInDays);
967
+ case 'hour':
968
+ return Math.trunc(valueInDays * HOURS_PER_DAY);
969
+ case 'minute':
970
+ return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR);
971
+ case 'second':
972
+ return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
973
+ case 'millisecond':
974
+ return Math.trunc(valueInDays * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND);
975
+ default:
976
+ return 0;
977
+ }
978
+ }
979
+
980
+ export function add(temporal: TemporalValue, quantity: TimeQuantity): TemporalValue {
981
+ if (isFHIRDate(temporal)) {
982
+ return addToDate(temporal, quantity);
983
+ }
984
+ if (isFHIRTime(temporal)) {
985
+ return addToTime(temporal, quantity);
986
+ }
987
+ if (isFHIRDateTime(temporal)) {
988
+ return addToDateTime(temporal, quantity);
989
+ }
990
+ return temporal;
991
+ }
992
+
993
+ export function subtract(temporal: TemporalValue, quantity: TimeQuantity): TemporalValue {
994
+ const negativeQuantity = createTimeQuantity(-quantity.value, quantity.unit);
995
+ return add(temporal, negativeQuantity);
996
+ }
997
+
998
+ // Helper to get the maximum day for a given month/year
999
+ export function getDaysInMonth(year: number, month: number): number {
1000
+ if (month === 2) {
1001
+ // February - check for leap year
1002
+ const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
1003
+ return isLeapYear ? 29 : 28;
1004
+ } else if ([4, 6, 9, 11].includes(month)) {
1005
+ return 30;
1006
+ } else {
1007
+ return 31;
1008
+ }
1009
+ }
1010
+
1011
+ // Helper to clamp a day to the valid range for a month
1012
+ function clampDay(year: number, month: number | undefined, day: number | undefined): number | undefined {
1013
+ if (day === undefined || month === undefined) {
1014
+ return day;
1015
+ }
1016
+ const maxDay = getDaysInMonth(year, month);
1017
+ return Math.min(day, maxDay);
1018
+ }
1019
+
1020
+ function addToDate(date: FHIRDate, quantity: TimeQuantity): FHIRDate {
1021
+ // Check for unsupported UCUM units
1022
+ if ((quantity.unit as string) === 'a') {
1023
+ throw Errors.invalidTemporalUnit('Date', quantity.unit as string);
1024
+ }
1025
+
1026
+ const normalizedUnit = normalizeUnit(quantity.unit);
1027
+
1028
+ // Only year/month/week/day units allowed for Date per spec
1029
+ if (!['year', 'month', 'week', 'day'].includes(normalizedUnit)) {
1030
+ // Includes hour/minute/second/millisecond and any other non-calendar unit
1031
+ throw Errors.unsupportedTemporalUnitForType('Date', quantity.unit as string);
1032
+ }
1033
+
1034
+ const cal = addCalendarParts(
1035
+ date.year,
1036
+ date.month,
1037
+ date.day,
1038
+ normalizedUnit as CalendarUnit,
1039
+ quantity.value
1040
+ );
1041
+ return createDate(cal.year, cal.month, cal.day);
1042
+ }
1043
+
1044
+ function addToTime(time: FHIRTime, quantity: TimeQuantity): FHIRTime {
1045
+ // Check for unsupported UCUM units
1046
+ if ((quantity.unit as string) === 'a') {
1047
+ throw new Error("Cannot use variable-duration unit 'a' with Time - use calendar duration keywords instead");
1048
+ }
1049
+
1050
+ const normalizedUnit = normalizeUnit(quantity.unit);
1051
+
1052
+ // Only time units allowed
1053
+ if (!['hour', 'minute', 'second', 'millisecond'].includes(normalizedUnit)) {
1054
+ throw new Error(`Cannot add ${quantity.unit} to Time`);
1055
+ }
1056
+
1057
+ // Convert everything to milliseconds
1058
+ let totalMs = time.hour * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1059
+
1060
+ if (time.minute !== undefined) {
1061
+ totalMs += time.minute * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1062
+ }
1063
+ if (time.second !== undefined) {
1064
+ totalMs += time.second * MILLISECONDS_PER_SECOND;
1065
+ }
1066
+ if (time.millisecond !== undefined) {
1067
+ totalMs += time.millisecond;
1068
+ }
1069
+
1070
+ // Add the quantity in milliseconds
1071
+ let quantityMs = 0;
1072
+ switch (normalizedUnit) {
1073
+ case 'hour':
1074
+ // For precisions above seconds, ignore decimal portion per spec
1075
+ quantityMs = Math.trunc(quantity.value) * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1076
+ break;
1077
+ case 'minute':
1078
+ // For precisions above seconds, ignore decimal portion per spec
1079
+ quantityMs = Math.trunc(quantity.value) * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1080
+ break;
1081
+ case 'second':
1082
+ quantityMs = quantity.value * MILLISECONDS_PER_SECOND;
1083
+ break;
1084
+ case 'millisecond':
1085
+ quantityMs = quantity.value;
1086
+ break;
1087
+ }
1088
+
1089
+ totalMs += quantityMs;
1090
+
1091
+ // Wrap around 24 hours
1092
+ const dayMs = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1093
+ totalMs = totalMs % dayMs;
1094
+ if (totalMs < 0) {
1095
+ totalMs += dayMs;
1096
+ }
1097
+
1098
+ // Convert back to components
1099
+ const newHour = Math.floor(totalMs / (MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND));
1100
+ totalMs %= MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1101
+
1102
+ let newMinute: number | undefined;
1103
+ let newSecond: number | undefined;
1104
+ let newMillisecond: number | undefined;
1105
+
1106
+ if (time.minute !== undefined) {
1107
+ newMinute = Math.floor(totalMs / (SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND));
1108
+ totalMs %= SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1109
+ }
1110
+
1111
+ if (time.second !== undefined) {
1112
+ newSecond = Math.floor(totalMs / MILLISECONDS_PER_SECOND);
1113
+ totalMs %= MILLISECONDS_PER_SECOND;
1114
+
1115
+ // Include milliseconds if original had them OR if we have fractional seconds
1116
+ if (time.millisecond !== undefined || totalMs > 0) {
1117
+ newMillisecond = Math.floor(totalMs);
1118
+ }
1119
+ } else if (time.millisecond !== undefined) {
1120
+ newMillisecond = Math.floor(totalMs);
1121
+ }
1122
+
1123
+ return createTime(newHour, newMinute, newSecond, newMillisecond);
1124
+ }
1125
+
1126
+ function addToDateTime(dt: FHIRDateTime, quantity: TimeQuantity): FHIRDateTime {
1127
+ // Check for unsupported UCUM units
1128
+ if ((quantity.unit as string) === 'a') {
1129
+ throw new Error("Cannot use variable-duration unit 'a' with DateTime - use calendar duration keywords instead");
1130
+ }
1131
+
1132
+ const normalizedUnit = normalizeUnit(quantity.unit);
1133
+ let newYear = dt.year;
1134
+ let newMonth = dt.month;
1135
+ let newDay = dt.day;
1136
+ let newHour = dt.hour;
1137
+ let newMinute = dt.minute;
1138
+ let newSecond = dt.second;
1139
+ let newMillisecond = dt.millisecond;
1140
+
1141
+ // Calendar units: delegate to shared helper
1142
+ if (['year', 'month', 'week', 'day'].includes(normalizedUnit)) {
1143
+ const cal = addCalendarParts(newYear, newMonth, newDay, normalizedUnit as CalendarUnit, quantity.value);
1144
+ newYear = cal.year;
1145
+ newMonth = cal.month;
1146
+ newDay = cal.day;
1147
+ return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
1148
+ }
1149
+
1150
+ // Time units: delegate to clock helper with precision preservation
1151
+ if (['hour', 'minute', 'second', 'millisecond'].includes(normalizedUnit)) {
1152
+ const clock = addClockParts(
1153
+ dt.hour,
1154
+ dt.minute,
1155
+ dt.second,
1156
+ dt.millisecond,
1157
+ normalizedUnit as ClockUnit,
1158
+ quantity.value,
1159
+ true
1160
+ );
1161
+ newHour = clock.hour;
1162
+ newMinute = clock.minute;
1163
+ newSecond = clock.second;
1164
+ newMillisecond = clock.millisecond;
1165
+
1166
+ if (clock.dayDelta !== 0) {
1167
+ const cal = addCalendarParts(newYear, newMonth, newDay, 'day', clock.dayDelta);
1168
+ newYear = cal.year;
1169
+ newMonth = cal.month;
1170
+ newDay = cal.day;
1171
+ }
1172
+ return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
1173
+ }
1174
+
1175
+ return createDateTime(newYear, newMonth, newDay, newHour, newMinute, newSecond, newMillisecond, dt.timezoneOffset);
1176
+ }
1177
+
1178
+ // ============================================================================
1179
+ // Component Extraction
1180
+ // ============================================================================
1181
+
1182
+ export function yearOf(temporal: TemporalValue): number | null {
1183
+ if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
1184
+ return temporal.year;
1185
+ }
1186
+ return null;
1187
+ }
1188
+
1189
+ export function monthOf(temporal: TemporalValue): number | null {
1190
+ if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
1191
+ return temporal.month ?? null;
1192
+ }
1193
+ return null;
1194
+ }
1195
+
1196
+ export function dayOf(temporal: TemporalValue): number | null {
1197
+ if (isFHIRDate(temporal) || isFHIRDateTime(temporal)) {
1198
+ return temporal.day ?? null;
1199
+ }
1200
+ return null;
1201
+ }
1202
+
1203
+ export function hourOf(temporal: TemporalValue): number | null {
1204
+ if (isFHIRTime(temporal)) {
1205
+ return temporal.hour;
1206
+ }
1207
+ if (isFHIRDateTime(temporal)) {
1208
+ return temporal.hour ?? null;
1209
+ }
1210
+ return null;
1211
+ }
1212
+
1213
+ export function minuteOf(temporal: TemporalValue): number | null {
1214
+ if (isFHIRTime(temporal)) {
1215
+ return temporal.minute ?? null;
1216
+ }
1217
+ if (isFHIRDateTime(temporal)) {
1218
+ return temporal.minute ?? null;
1219
+ }
1220
+ return null;
1221
+ }
1222
+
1223
+ export function secondOf(temporal: TemporalValue): number | null {
1224
+ if (isFHIRTime(temporal)) {
1225
+ return temporal.second ?? null;
1226
+ }
1227
+ if (isFHIRDateTime(temporal)) {
1228
+ return temporal.second ?? null;
1229
+ }
1230
+ return null;
1231
+ }
1232
+
1233
+ export function millisecondOf(temporal: TemporalValue): number | null {
1234
+ if (isFHIRTime(temporal)) {
1235
+ return temporal.millisecond ?? null;
1236
+ }
1237
+ if (isFHIRDateTime(temporal)) {
1238
+ return temporal.millisecond ?? null;
1239
+ }
1240
+ return null;
1241
+ }
1242
+
1243
+ // ============================================================================
1244
+ // Helper Functions
1245
+ // ============================================================================
1246
+
1247
+ // Shared helper scaffolding for refactor (TDD-first)
1248
+ export type CalendarUnit = 'year' | 'month' | 'week' | 'day';
1249
+ export type ClockUnit = 'hour' | 'minute' | 'second' | 'millisecond';
1250
+
1251
+ export interface CalendarPartsResult {
1252
+ year: number;
1253
+ month?: number;
1254
+ day?: number;
1255
+ }
1256
+
1257
+ export interface ClockPartsResult {
1258
+ hour?: number;
1259
+ minute?: number;
1260
+ second?: number;
1261
+ millisecond?: number;
1262
+ // +1 when time addition crosses midnight forward, -1 when backward, 0 otherwise
1263
+ dayDelta: number;
1264
+ }
1265
+
1266
+ // Intentionally left unimplemented for now (tests drive implementation)
1267
+ export function addCalendarParts(
1268
+ year: number,
1269
+ month: number | undefined,
1270
+ day: number | undefined,
1271
+ unit: CalendarUnit,
1272
+ amount: number
1273
+ ): CalendarPartsResult {
1274
+ const normalizedUnit = normalizeUnit(unit);
1275
+
1276
+ let newYear = year;
1277
+ let newMonth = month;
1278
+ let newDay = day;
1279
+
1280
+ if (normalizedUnit === 'week') {
1281
+ // Convert weeks to days using calendar semantics (truncate fractional weeks)
1282
+ const weeksToAdd = Math.trunc(amount);
1283
+ return addCalendarParts(newYear, newMonth, newDay, 'day', weeksToAdd * 7);
1284
+ }
1285
+
1286
+ if (normalizedUnit === 'year') {
1287
+ newYear += Math.trunc(amount);
1288
+ } else if (normalizedUnit === 'month') {
1289
+ const monthsToAdd = Math.trunc(amount);
1290
+ if (newMonth !== undefined) {
1291
+ let totalMonths = (newYear * 12) + (newMonth - 1) + monthsToAdd;
1292
+ newYear = Math.floor(totalMonths / 12);
1293
+ newMonth = (totalMonths % 12) + 1;
1294
+ if (newMonth <= 0) {
1295
+ newMonth += 12;
1296
+ newYear--;
1297
+ }
1298
+ } else {
1299
+ // Coerce months into years when only year precision
1300
+ const yearsToAdd = convertAndTruncate(createTimeQuantity(amount, 'month'), 'year');
1301
+ newYear += yearsToAdd;
1302
+ }
1303
+ } else if (normalizedUnit === 'day') {
1304
+ const daysToAdd = Math.trunc(amount);
1305
+ if (newDay !== undefined && newMonth !== undefined) {
1306
+ // Proper calendar day arithmetic
1307
+ let currentYear = newYear;
1308
+ let currentMonth = newMonth;
1309
+ let currentDay = newDay + daysToAdd;
1310
+
1311
+ while (currentDay > getDaysInMonth(currentYear, currentMonth)) {
1312
+ currentDay -= getDaysInMonth(currentYear, currentMonth);
1313
+ currentMonth++;
1314
+ if (currentMonth > 12) {
1315
+ currentMonth = 1;
1316
+ currentYear++;
1317
+ }
1318
+ }
1319
+
1320
+ while (currentDay < 1) {
1321
+ currentMonth--;
1322
+ if (currentMonth < 1) {
1323
+ currentMonth = 12;
1324
+ currentYear--;
1325
+ }
1326
+ currentDay += getDaysInMonth(currentYear, currentMonth);
1327
+ }
1328
+
1329
+ newYear = currentYear;
1330
+ newMonth = currentMonth;
1331
+ newDay = currentDay;
1332
+ } else if (newMonth !== undefined) {
1333
+ // Convert days to months when month precision but no day
1334
+ const monthsToAdd = convertAndTruncate(createTimeQuantity(amount, 'day'), 'month');
1335
+ let totalMonths = (newYear * 12) + (newMonth - 1) + monthsToAdd;
1336
+ newYear = Math.floor(totalMonths / 12);
1337
+ newMonth = (totalMonths % 12) + 1;
1338
+ if (newMonth <= 0) {
1339
+ newMonth += 12;
1340
+ newYear--;
1341
+ }
1342
+ } else {
1343
+ // Convert days to years when only year precision
1344
+ const yearsToAdd = convertAndTruncate(createTimeQuantity(amount, 'day'), 'year');
1345
+ newYear += yearsToAdd;
1346
+ }
1347
+ }
1348
+
1349
+ // Clamp day for validity when we have a day and month
1350
+ newDay = clampDay(newYear, newMonth, newDay);
1351
+
1352
+ return { year: newYear, month: newMonth, day: newDay };
1353
+ }
1354
+
1355
+ // Intentionally left unimplemented for now (tests drive implementation)
1356
+ export function addClockParts(
1357
+ hour: number | undefined,
1358
+ minute: number | undefined,
1359
+ second: number | undefined,
1360
+ millisecond: number | undefined,
1361
+ unit: ClockUnit,
1362
+ amount: number,
1363
+ preservePrecision: boolean = true
1364
+ ): ClockPartsResult {
1365
+ const HOUR_MS = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1366
+ const MINUTE_MS = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
1367
+ const SECOND_MS = MILLISECONDS_PER_SECOND;
1368
+ const DAY_MS = HOURS_PER_DAY * HOUR_MS;
1369
+
1370
+ // Build original total milliseconds from time-of-day
1371
+ let totalMs = (hour ?? 0) * HOUR_MS;
1372
+ if (minute !== undefined) totalMs += minute * MINUTE_MS;
1373
+ if (second !== undefined) totalMs += second * SECOND_MS;
1374
+ if (millisecond !== undefined) totalMs += millisecond;
1375
+
1376
+ // Compute quantity in milliseconds honoring truncation rules
1377
+ let deltaMs = 0;
1378
+ if (unit === 'hour') {
1379
+ deltaMs = Math.trunc(amount) * HOUR_MS;
1380
+ } else if (unit === 'minute') {
1381
+ // precision-preserving: we'll add minutes as milliseconds, but reconstruction preserves precision
1382
+ deltaMs = Math.trunc(amount) * MINUTE_MS;
1383
+ } else if (unit === 'second') {
1384
+ deltaMs = amount * SECOND_MS; // fractions become milliseconds
1385
+ } else if (unit === 'millisecond') {
1386
+ deltaMs = amount;
1387
+ }
1388
+
1389
+ const newTotal = totalMs + deltaMs;
1390
+ // Compute dayDelta via floor division and get a non-negative wrapped remainder
1391
+ let dayDelta = Math.floor(newTotal / DAY_MS);
1392
+ let wrapped = newTotal - dayDelta * DAY_MS;
1393
+
1394
+ // Reconstruct time parts with precision preservation
1395
+ // Preserve hour precision: if hour was undefined, don't materialize it
1396
+ let newHour: number | undefined =
1397
+ hour !== undefined ? Math.floor(wrapped / HOUR_MS) : undefined;
1398
+ if (hour !== undefined) {
1399
+ wrapped %= HOUR_MS;
1400
+ }
1401
+
1402
+ let newMinute: number | undefined = undefined;
1403
+ let newSecond: number | undefined = undefined;
1404
+ let newMillisecond: number | undefined = undefined;
1405
+
1406
+ if (minute !== undefined) {
1407
+ newMinute = Math.floor(wrapped / MINUTE_MS);
1408
+ wrapped %= MINUTE_MS;
1409
+ }
1410
+
1411
+ if (second !== undefined) {
1412
+ newSecond = Math.floor(wrapped / SECOND_MS);
1413
+ wrapped %= SECOND_MS;
1414
+ }
1415
+
1416
+ if (millisecond !== undefined) {
1417
+ newMillisecond = Math.floor(wrapped);
1418
+ wrapped = 0;
1419
+ } else if (!preservePrecision && (second !== undefined || minute !== undefined)) {
1420
+ // Only when explicitly allowed to introduce finer precision
1421
+ newMillisecond = Math.floor(wrapped);
1422
+ wrapped = 0;
1423
+ }
1424
+
1425
+ return {
1426
+ hour: newHour,
1427
+ minute: newMinute,
1428
+ second: newSecond,
1429
+ millisecond: newMillisecond,
1430
+ dayDelta
1431
+ };
1432
+ }
1433
+
1434
+ // Cache for UTC-normalized DateTimes to avoid repeated conversions
1435
+ const utcNormalizationCache = new WeakMap<FHIRDateTime, FHIRDateTime>();
1436
+
1437
+ function normalizeToUTC(dt: FHIRDateTime): FHIRDateTime {
1438
+ if (dt.timezoneOffset === undefined || dt.timezoneOffset === 0) {
1439
+ return dt;
1440
+ }
1441
+
1442
+ // Check cache first
1443
+ const cached = utcNormalizationCache.get(dt);
1444
+ if (cached) {
1445
+ return cached;
1446
+ }
1447
+
1448
+ // Convert to total minutes and adjust
1449
+ let totalMinutes = (dt.hour ?? 0) * 60 + (dt.minute ?? 0) - dt.timezoneOffset;
1450
+
1451
+ // Handle day boundary crossing
1452
+ let dayAdjust = 0;
1453
+ if (totalMinutes < 0) {
1454
+ dayAdjust = -Math.ceil(Math.abs(totalMinutes) / (24 * 60));
1455
+ totalMinutes += Math.abs(dayAdjust) * 24 * 60;
1456
+ } else if (totalMinutes >= 24 * 60) {
1457
+ dayAdjust = Math.floor(totalMinutes / (24 * 60));
1458
+ totalMinutes %= 24 * 60;
1459
+ }
1460
+
1461
+ const newHour = Math.floor(totalMinutes / 60);
1462
+ const newMinute = totalMinutes % 60;
1463
+
1464
+ // Adjust day if needed
1465
+ let newDay = dt.day;
1466
+ let newMonth = dt.month;
1467
+ let newYear = dt.year;
1468
+
1469
+ if (dayAdjust !== 0 && newDay !== undefined && newMonth !== undefined) {
1470
+ // Use proper calendar arithmetic for day adjustment
1471
+ const tempDate = createDate(newYear, newMonth, newDay);
1472
+ const dayQuantity = createTimeQuantity(dayAdjust, 'day');
1473
+ const adjustedDate = addToDate(tempDate, dayQuantity);
1474
+ newYear = adjustedDate.year;
1475
+ newMonth = adjustedDate.month;
1476
+ newDay = adjustedDate.day;
1477
+ }
1478
+
1479
+ const result: FHIRDateTime = {
1480
+ kind: 'FHIRDateTime',
1481
+ year: newYear,
1482
+ month: newMonth,
1483
+ day: newDay,
1484
+ hour: dt.hour !== undefined ? newHour : undefined,
1485
+ minute: dt.minute !== undefined ? newMinute : undefined,
1486
+ second: dt.second,
1487
+ millisecond: dt.millisecond,
1488
+ timezoneOffset: 0,
1489
+ precision: dt.precision
1490
+ };
1491
+
1492
+ // Store in cache for future lookups
1493
+ utcNormalizationCache.set(dt, result);
1494
+
1495
+ return result;
1496
+ }
1497
+
1498
+ // ============================================================================
1499
+ // Backwards Compatibility Exports (temporary during migration)
1500
+ // ============================================================================
1501
+
1502
+ // Export classes that map to factory functions
1503
+ export const FHIRDate = {
1504
+ new: createDate,
1505
+ create: createDate
1506
+ };
1507
+
1508
+ export const FHIRTime = {
1509
+ new: createTime,
1510
+ create: createTime
1511
+ };
1512
+
1513
+ export const FHIRDateTime = {
1514
+ new: createDateTime,
1515
+ create: createDateTime
1516
+ };
1517
+
1518
+ // ============================================================================
1519
+ // Boundary Functions
1520
+ // ============================================================================
1521
+
1522
+ /**
1523
+ * Calculate the low boundary for a Date value
1524
+ */
1525
+ export function getDateLowBoundary(date: FHIRDate, precision?: number): FHIRDate | null {
1526
+ // Validate precision
1527
+ if (precision !== undefined) {
1528
+ if (precision < 0 || precision > 8) {
1529
+ return null;
1530
+ }
1531
+ } else {
1532
+ // Default precision for Date is 8 (day)
1533
+ precision = 8;
1534
+ }
1535
+
1536
+ // Build the boundary date based on precision
1537
+ let year = date.year;
1538
+ let month: number | undefined;
1539
+ let day: number | undefined;
1540
+
1541
+ if (precision >= 6) {
1542
+ month = date.month ?? 1;
1543
+ }
1544
+
1545
+ if (precision >= 8) {
1546
+ day = date.day ?? 1;
1547
+ }
1548
+
1549
+ return createDate(year, month, day);
1550
+ }
1551
+
1552
+ /**
1553
+ * Calculate the high boundary for a Date value
1554
+ */
1555
+ export function getDateHighBoundary(date: FHIRDate, precision?: number): FHIRDate | null {
1556
+ // Validate precision
1557
+ if (precision !== undefined) {
1558
+ if (precision < 0 || precision > 8) {
1559
+ return null;
1560
+ }
1561
+ } else {
1562
+ // Default precision for Date is 8 (day)
1563
+ precision = 8;
1564
+ }
1565
+
1566
+ // Build the boundary date based on precision
1567
+ let year = date.year;
1568
+ let month: number | undefined;
1569
+ let day: number | undefined;
1570
+
1571
+ if (precision >= 6) {
1572
+ month = date.month ?? 12;
1573
+ }
1574
+
1575
+ if (precision >= 8) {
1576
+ if (date.day !== undefined) {
1577
+ day = date.day;
1578
+ } else {
1579
+ // Need to calculate last day of month
1580
+ const actualMonth = month ?? 12;
1581
+ day = getDaysInMonth(year, actualMonth);
1582
+ }
1583
+ }
1584
+
1585
+ return createDate(year, month, day);
1586
+ }
1587
+
1588
+ /**
1589
+ * Calculate the low boundary for a DateTime value
1590
+ */
1591
+ export function getDateTimeLowBoundary(dateTime: FHIRDateTime, precision?: number): FHIRDateTime | null {
1592
+ // Validate precision
1593
+ if (precision !== undefined) {
1594
+ if (precision < 0 || precision > 17) {
1595
+ return null;
1596
+ }
1597
+ } else {
1598
+ // Default precision for DateTime is 17 (millisecond)
1599
+ precision = 17;
1600
+ }
1601
+
1602
+ // Build the boundary datetime based on precision
1603
+ let year = dateTime.year;
1604
+ let month: number | undefined;
1605
+ let day: number | undefined;
1606
+ let hour: number | undefined;
1607
+ let minute: number | undefined;
1608
+ let second: number | undefined;
1609
+ let millisecond: number | undefined;
1610
+ let timezoneOffset: number | undefined = dateTime.timezoneOffset;
1611
+
1612
+ if (precision >= 6) {
1613
+ month = dateTime.month ?? 1;
1614
+ }
1615
+
1616
+ if (precision >= 8) {
1617
+ day = dateTime.day ?? 1;
1618
+ }
1619
+
1620
+ if (precision >= 10) {
1621
+ hour = dateTime.hour ?? 0;
1622
+ }
1623
+
1624
+ if (precision >= 12) {
1625
+ minute = dateTime.minute ?? 0;
1626
+ }
1627
+
1628
+ if (precision >= 14) {
1629
+ second = dateTime.second ?? 0;
1630
+ }
1631
+
1632
+ if (precision >= 17) {
1633
+ millisecond = dateTime.millisecond ?? 0;
1634
+
1635
+ // If no timezone was specified and we're at millisecond precision,
1636
+ // use the maximum positive offset (+14:00 = 840 minutes)
1637
+ if (timezoneOffset === undefined && dateTime.hour !== undefined) {
1638
+ timezoneOffset = 840; // +14:00
1639
+ }
1640
+ }
1641
+
1642
+ return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
1643
+ }
1644
+
1645
+ /**
1646
+ * Calculate the high boundary for a DateTime value
1647
+ */
1648
+ export function getDateTimeHighBoundary(dateTime: FHIRDateTime, precision?: number): FHIRDateTime | null {
1649
+ // Validate precision
1650
+ if (precision !== undefined) {
1651
+ if (precision < 0 || precision > 17) {
1652
+ return null;
1653
+ }
1654
+ } else {
1655
+ // Default precision for DateTime is 17 (millisecond)
1656
+ precision = 17;
1657
+ }
1658
+
1659
+ // Build the boundary datetime based on precision
1660
+ let year = dateTime.year;
1661
+ let month: number | undefined;
1662
+ let day: number | undefined;
1663
+ let hour: number | undefined;
1664
+ let minute: number | undefined;
1665
+ let second: number | undefined;
1666
+ let millisecond: number | undefined;
1667
+ let timezoneOffset: number | undefined = dateTime.timezoneOffset;
1668
+
1669
+ if (precision >= 6) {
1670
+ month = dateTime.month ?? 12;
1671
+ }
1672
+
1673
+ if (precision >= 8) {
1674
+ if (dateTime.day !== undefined) {
1675
+ day = dateTime.day;
1676
+ } else {
1677
+ // Need to calculate last day of month
1678
+ const actualMonth = month ?? 12;
1679
+ day = getDaysInMonth(year, actualMonth);
1680
+ }
1681
+ }
1682
+
1683
+ if (precision >= 10) {
1684
+ hour = dateTime.hour ?? 23;
1685
+ }
1686
+
1687
+ if (precision >= 12) {
1688
+ minute = dateTime.minute ?? 59;
1689
+ }
1690
+
1691
+ if (precision >= 14) {
1692
+ second = dateTime.second ?? 59;
1693
+ }
1694
+
1695
+ if (precision >= 17) {
1696
+ millisecond = dateTime.millisecond ?? 999;
1697
+
1698
+ // If no timezone was specified and we're at millisecond precision,
1699
+ // use the maximum negative offset (-12:00 = -720 minutes)
1700
+ if (timezoneOffset === undefined && dateTime.hour !== undefined) {
1701
+ timezoneOffset = -720; // -12:00
1702
+ }
1703
+ }
1704
+
1705
+ return createDateTime(year, month, day, hour, minute, second, millisecond, timezoneOffset);
1706
+ }
1707
+
1708
+ /**
1709
+ * Calculate the low boundary for a Time value
1710
+ */
1711
+ export function getTimeLowBoundary(time: FHIRTime, precision?: number): FHIRTime | null {
1712
+ // Validate precision
1713
+ if (precision !== undefined) {
1714
+ if (precision < 0 || precision > 9) {
1715
+ return null;
1716
+ }
1717
+ } else {
1718
+ // Default precision for Time is 9 (millisecond)
1719
+ precision = 9;
1720
+ }
1721
+
1722
+ // Build the boundary time based on precision
1723
+ let hour = time.hour;
1724
+ let minute: number | undefined;
1725
+ let second: number | undefined;
1726
+ let millisecond: number | undefined;
1727
+
1728
+ if (precision >= 5) {
1729
+ minute = time.minute ?? 0;
1730
+ }
1731
+
1732
+ if (precision >= 7) {
1733
+ second = time.second ?? 0;
1734
+ }
1735
+
1736
+ if (precision >= 9) {
1737
+ millisecond = time.millisecond ?? 0;
1738
+ }
1739
+
1740
+ return createTime(hour, minute, second, millisecond);
1741
+ }
1742
+
1743
+ /**
1744
+ * Calculate the high boundary for a Time value
1745
+ */
1746
+ export function getTimeHighBoundary(time: FHIRTime, precision?: number): FHIRTime | null {
1747
+ // Validate precision
1748
+ if (precision !== undefined) {
1749
+ if (precision < 0 || precision > 9) {
1750
+ return null;
1751
+ }
1752
+ } else {
1753
+ // Default precision for Time is 9 (millisecond)
1754
+ precision = 9;
1755
+ }
1756
+
1757
+ // Build the boundary time based on precision
1758
+ let hour = time.hour;
1759
+ let minute: number | undefined;
1760
+ let second: number | undefined;
1761
+ let millisecond: number | undefined;
1762
+
1763
+ if (precision >= 5) {
1764
+ minute = time.minute ?? 59;
1765
+ }
1766
+
1767
+ if (precision >= 7) {
1768
+ second = time.second ?? 59;
1769
+ }
1770
+
1771
+ if (precision >= 9) {
1772
+ millisecond = time.millisecond ?? 999;
1773
+ }
1774
+
1775
+ return createTime(hour, minute, second, millisecond);
1776
+ }