@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,410 @@
1
+ import { ucum } from '@atomic-ehr/ucum';
2
+ import type { Quantity } from '@atomic-ehr/ucum';
3
+
4
+ /**
5
+ * Wrapper for FHIRPath quantity values with UCUM integration
6
+ */
7
+ export interface QuantityValue {
8
+ value: number;
9
+ unit: string;
10
+ _ucumQuantity?: Quantity; // Lazy-initialized
11
+ }
12
+
13
+ /**
14
+ * Calendar duration units used in FHIRPath
15
+ * These are NOT UCUM units and should not be converted
16
+ */
17
+ export const CALENDAR_DURATION_UNITS = new Set([
18
+ 'year', 'years',
19
+ 'month', 'months',
20
+ 'week', 'weeks',
21
+ 'day', 'days',
22
+ 'hour', 'hours',
23
+ 'minute', 'minutes',
24
+ 'second', 'seconds',
25
+ 'millisecond', 'milliseconds'
26
+ ]);
27
+
28
+ /**
29
+ * Calendar to UCUM duration mappings
30
+ * Maps FHIRPath calendar duration units to their UCUM equivalents
31
+ */
32
+ export const CALENDAR_TO_UCUM: Record<string, string> = {
33
+ 'year': 'a',
34
+ 'years': 'a',
35
+ 'month': 'mo',
36
+ 'months': 'mo',
37
+ 'week': 'wk',
38
+ 'weeks': 'wk',
39
+ 'day': 'd',
40
+ 'days': 'd',
41
+ 'hour': 'h',
42
+ 'hours': 'h',
43
+ 'minute': 'min',
44
+ 'minutes': 'min',
45
+ 'second': 's',
46
+ 'seconds': 's',
47
+ 'millisecond': 'ms',
48
+ 'milliseconds': 'ms'
49
+ };
50
+
51
+ /**
52
+ * Create a quantity value
53
+ */
54
+ export function createQuantity(value: number, unit: string): QuantityValue {
55
+ return {
56
+ value,
57
+ unit
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Get or create the UCUM quantity for a QuantityValue
63
+ */
64
+ export function getUcumQuantity(quantity: QuantityValue): Quantity | null {
65
+ // Calendar duration units are not UCUM units
66
+ if (CALENDAR_DURATION_UNITS.has(quantity.unit)) {
67
+ return null;
68
+ }
69
+
70
+ if (!quantity._ucumQuantity) {
71
+ try {
72
+ quantity._ucumQuantity = ucum.quantity(quantity.value, quantity.unit);
73
+ } catch (e) {
74
+ // Invalid unit - return null
75
+ return null;
76
+ }
77
+ }
78
+ return quantity._ucumQuantity || null;
79
+ }
80
+
81
+ /**
82
+ * Check if a quantity has a valid unit
83
+ * Valid units are either UCUM units or calendar duration units
84
+ */
85
+ export function isValidQuantity(quantity: QuantityValue): boolean {
86
+ // Calendar duration units are valid FHIRPath quantities
87
+ if (CALENDAR_DURATION_UNITS.has(quantity.unit)) {
88
+ return true;
89
+ }
90
+ // Otherwise check if it's a valid UCUM unit
91
+ return getUcumQuantity(quantity) !== null;
92
+ }
93
+
94
+ /**
95
+ * Add two quantities
96
+ */
97
+ export function addQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
98
+ // Special case: adding calendar durations with the same unit
99
+ if (CALENDAR_DURATION_UNITS.has(left.unit) && left.unit === right.unit) {
100
+ return {
101
+ value: left.value + right.value,
102
+ unit: left.unit
103
+ };
104
+ }
105
+
106
+ // Different calendar units cannot be added
107
+ if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
108
+ return null;
109
+ }
110
+
111
+ const leftUcum = getUcumQuantity(left);
112
+ const rightUcum = getUcumQuantity(right);
113
+
114
+ if (!leftUcum || !rightUcum) {
115
+ return null;
116
+ }
117
+
118
+ try {
119
+ const result = ucum.add(leftUcum, rightUcum);
120
+ return {
121
+ value: result.value,
122
+ unit: result.unit
123
+ };
124
+ } catch (e) {
125
+ // Incompatible dimensions
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Subtract two quantities
132
+ */
133
+ export function subtractQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
134
+ // Special case: subtracting calendar durations with the same unit
135
+ if (CALENDAR_DURATION_UNITS.has(left.unit) && left.unit === right.unit) {
136
+ return {
137
+ value: left.value - right.value,
138
+ unit: left.unit
139
+ };
140
+ }
141
+
142
+ // Different calendar units cannot be subtracted
143
+ if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
144
+ return null;
145
+ }
146
+
147
+ const leftUcum = getUcumQuantity(left);
148
+ const rightUcum = getUcumQuantity(right);
149
+
150
+ if (!leftUcum || !rightUcum) {
151
+ return null;
152
+ }
153
+
154
+ try {
155
+ const result = ucum.subtract(leftUcum, rightUcum);
156
+ return {
157
+ value: result.value,
158
+ unit: result.unit
159
+ };
160
+ } catch (e) {
161
+ // Incompatible dimensions
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Multiply two quantities
168
+ */
169
+ export function multiplyQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
170
+ // Special case: multiplying calendar duration by a dimensionless number
171
+ // e.g., 1 year * 2 = 2 years, or 2 * 1 year = 2 years
172
+ if (CALENDAR_DURATION_UNITS.has(left.unit) && right.unit === '1') {
173
+ // Calendar duration * number
174
+ return {
175
+ value: left.value * right.value,
176
+ unit: left.unit
177
+ };
178
+ }
179
+ if (CALENDAR_DURATION_UNITS.has(right.unit) && left.unit === '1') {
180
+ // Number * calendar duration
181
+ return {
182
+ value: left.value * right.value,
183
+ unit: right.unit
184
+ };
185
+ }
186
+
187
+ // Check if both are calendar duration units - this is not allowed
188
+ if (CALENDAR_DURATION_UNITS.has(left.unit) && CALENDAR_DURATION_UNITS.has(right.unit)) {
189
+ // Calendar duration units cannot be multiplied together - the result would be meaningless
190
+ throw new Error(`Cannot multiply calendar duration units: ${left.unit} * ${right.unit}`);
191
+ }
192
+
193
+ // Mixed calendar and non-calendar units are not allowed
194
+ if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
195
+ throw new Error(`Cannot multiply calendar duration with non-calendar units: ${left.unit} * ${right.unit}`);
196
+ }
197
+
198
+ const leftUcum = getUcumQuantity(left);
199
+ const rightUcum = getUcumQuantity(right);
200
+
201
+ if (!leftUcum || !rightUcum) {
202
+ return null;
203
+ }
204
+
205
+ try {
206
+ const result = ucum.multiply(leftUcum, rightUcum);
207
+ return {
208
+ value: result.value,
209
+ unit: result.unit
210
+ };
211
+ } catch (e) {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Divide two quantities
218
+ */
219
+ export function divideQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
220
+ // Special case: dividing calendar duration by a dimensionless number
221
+ // e.g., 10 months / 2 = 5 months
222
+ if (CALENDAR_DURATION_UNITS.has(left.unit) && right.unit === '1') {
223
+ // Calendar duration / number
224
+ return {
225
+ value: left.value / right.value,
226
+ unit: left.unit
227
+ };
228
+ }
229
+
230
+ // Division of number by calendar duration is not allowed
231
+ // Division of two calendar durations is not allowed
232
+ if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
233
+ // Calendar duration units cannot be divided except by dimensionless numbers
234
+ throw new Error(`Cannot divide calendar duration units: ${left.unit} / ${right.unit}`);
235
+ }
236
+
237
+ const leftUcum = getUcumQuantity(left);
238
+ const rightUcum = getUcumQuantity(right);
239
+
240
+ if (!leftUcum || !rightUcum) {
241
+ return null;
242
+ }
243
+
244
+ try {
245
+ const result = ucum.divide(leftUcum, rightUcum);
246
+ return {
247
+ value: result.value,
248
+ unit: result.unit
249
+ };
250
+ } catch (e) {
251
+ return null;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Compare two quantities
257
+ * Returns -1 if left < right, 0 if equal, 1 if left > right, null if incomparable
258
+ */
259
+ export function compareQuantities(left: QuantityValue, right: QuantityValue): number | null {
260
+ // Handle calendar to UCUM comparisons
261
+ const leftIsCalendar = CALENDAR_DURATION_UNITS.has(left.unit);
262
+ const rightIsCalendar = CALENDAR_DURATION_UNITS.has(right.unit);
263
+
264
+ // Both calendar units - only compare if they're compatible units
265
+ if (leftIsCalendar && rightIsCalendar) {
266
+ // Only allow conversion between specific compatible units
267
+ const areCompatible = (unit1: string, unit2: string): boolean => {
268
+ // Normalize to singular form
269
+ const normalize = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
270
+ const n1 = normalize(unit1);
271
+ const n2 = normalize(unit2);
272
+
273
+ // Same unit (singular/plural)
274
+ if (n1 === n2) return true;
275
+
276
+ // Week <-> days conversion
277
+ if ((n1 === 'week' && n2 === 'day') || (n1 === 'day' && n2 === 'week')) return true;
278
+
279
+ // No other conversions between calendar units
280
+ return false;
281
+ };
282
+
283
+ if (!areCompatible(left.unit, right.unit)) {
284
+ // Different calendar units that aren't compatible
285
+ return null;
286
+ }
287
+
288
+ // Handle week/day conversion
289
+ const normalizeToSingular = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
290
+ const leftNorm = normalizeToSingular(left.unit);
291
+ const rightNorm = normalizeToSingular(right.unit);
292
+
293
+ if (leftNorm === rightNorm) {
294
+ // Same unit, just compare values
295
+ if (left.value < right.value) return -1;
296
+ if (left.value > right.value) return 1;
297
+ return 0;
298
+ }
299
+
300
+ // Week <-> day conversion
301
+ if ((leftNorm === 'week' && rightNorm === 'day') ||
302
+ (leftNorm === 'day' && rightNorm === 'week')) {
303
+ const leftInDays = leftNorm === 'week' ? left.value * 7 : left.value;
304
+ const rightInDays = rightNorm === 'week' ? right.value * 7 : right.value;
305
+
306
+ if (leftInDays < rightInDays) return -1;
307
+ if (leftInDays > rightInDays) return 1;
308
+ return 0;
309
+ }
310
+
311
+ // Shouldn't reach here
312
+ return null;
313
+ }
314
+
315
+ // Calendar to UCUM comparison
316
+ if (leftIsCalendar && !rightIsCalendar) {
317
+ // Check for week/day special case
318
+ if ((left.unit === 'days' || left.unit === 'day') && right.unit === 'wk') {
319
+ // Convert days to weeks
320
+ const leftInWeeks = left.value / 7;
321
+ if (leftInWeeks < right.value) return -1;
322
+ if (leftInWeeks > right.value) return 1;
323
+ return 0;
324
+ }
325
+ if ((left.unit === 'weeks' || left.unit === 'week') && right.unit === 'd') {
326
+ // Convert weeks to days
327
+ const leftInDays = left.value * 7;
328
+ if (leftInDays < right.value) return -1;
329
+ if (leftInDays > right.value) return 1;
330
+ return 0;
331
+ }
332
+
333
+ // Direct mapping check
334
+ const leftUcumUnit = CALENDAR_TO_UCUM[left.unit];
335
+ if (leftUcumUnit === right.unit) {
336
+ // Direct mapping, compare values
337
+ if (left.value < right.value) return -1;
338
+ if (left.value > right.value) return 1;
339
+ return 0;
340
+ }
341
+ // No mapping, incomparable
342
+ return null;
343
+ }
344
+
345
+ // UCUM to calendar comparison
346
+ if (!leftIsCalendar && rightIsCalendar) {
347
+ // Check for week/day special case
348
+ if (left.unit === 'wk' && (right.unit === 'days' || right.unit === 'day')) {
349
+ // Convert weeks to days
350
+ const leftInDays = left.value * 7;
351
+ if (leftInDays < right.value) return -1;
352
+ if (leftInDays > right.value) return 1;
353
+ return 0;
354
+ }
355
+ if (left.unit === 'd' && (right.unit === 'weeks' || right.unit === 'week')) {
356
+ // Convert days to weeks
357
+ const leftInWeeks = left.value / 7;
358
+ if (leftInWeeks < right.value) return -1;
359
+ if (leftInWeeks > right.value) return 1;
360
+ return 0;
361
+ }
362
+
363
+ // Direct mapping check
364
+ const rightUcumUnit = CALENDAR_TO_UCUM[right.unit];
365
+ if (left.unit === rightUcumUnit) {
366
+ // Direct mapping, compare values
367
+ if (left.value < right.value) return -1;
368
+ if (left.value > right.value) return 1;
369
+ return 0;
370
+ }
371
+ // No mapping, incomparable
372
+ return null;
373
+ }
374
+
375
+ const leftUcum = getUcumQuantity(left);
376
+ const rightUcum = getUcumQuantity(right);
377
+
378
+ if (!leftUcum || !rightUcum) {
379
+ return null;
380
+ }
381
+
382
+ try {
383
+ // Try to convert right to left's unit for comparison
384
+ const rightValue = ucum.convert(rightUcum.value, rightUcum.unit, leftUcum.unit);
385
+ if (leftUcum.value < rightValue) {
386
+ return -1;
387
+ } else if (leftUcum.value > rightValue) {
388
+ return 1;
389
+ } else {
390
+ return 0;
391
+ }
392
+ } catch (e) {
393
+ // Incompatible dimensions
394
+ return null;
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Check if two quantities are equal
400
+ */
401
+ export function equalQuantities(left: QuantityValue, right: QuantityValue): boolean {
402
+ return compareQuantities(left, right) === 0;
403
+ }
404
+
405
+ /**
406
+ * Convert quantity to string representation
407
+ */
408
+ export function quantityToString(quantity: QuantityValue): string {
409
+ return `${quantity.value} '${quantity.unit}'`;
410
+ }