@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,692 @@
1
+ import type { FHIRPathValue } from '../interpreter/boxing';
2
+ import { unbox } from '../interpreter/boxing';
3
+ import type { QuantityValue } from '../complex-types/quantity-value';
4
+ import { compareQuantities } from '../complex-types/quantity-value';
5
+ import type { TemporalValue } from '../complex-types/temporal';
6
+ import { equals as temporalEquals, compare as temporalCompare, isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
7
+
8
+ /**
9
+ * Result of comparing two values
10
+ */
11
+ export type ComparisonResult =
12
+ | { kind: 'equal' }
13
+ | { kind: 'less' }
14
+ | { kind: 'greater' }
15
+ | { kind: 'incomparable'; reason?: string };
16
+
17
+ /**
18
+ * Compare two FHIRPath values
19
+ * Returns a ComparisonResult indicating the relationship between the values
20
+ */
21
+ export function compare(a: unknown, b: unknown): ComparisonResult {
22
+ // Early exit: reference equality
23
+ if (a === b) {
24
+ return { kind: 'equal' };
25
+ }
26
+
27
+ // Handle null/undefined
28
+ if (a === null || a === undefined || b === null || b === undefined) {
29
+ return { kind: 'incomparable', reason: 'null or undefined value' };
30
+ }
31
+
32
+ // Early exit: type mismatch for primitives
33
+ const typeA = typeof a;
34
+ const typeB = typeof b;
35
+ if (typeA !== typeB && typeA !== 'object' && typeB !== 'object') {
36
+ return { kind: 'incomparable', reason: 'type mismatch' };
37
+ }
38
+
39
+ // Check if values are temporal types
40
+ if (isTemporalValue(a) && isTemporalValue(b)) {
41
+ return compareTemporal(a, b);
42
+ }
43
+
44
+ // Check if values are quantities
45
+ if (isQuantity(a) && isQuantity(b)) {
46
+ return compareQuantityValues(a, b);
47
+ }
48
+
49
+ // Handle mixed quantity and number comparison
50
+ if (isQuantity(a) && typeof b === 'number') {
51
+ return compareQuantityToNumber(a, b);
52
+ }
53
+ if (typeof a === 'number' && isQuantity(b)) {
54
+ const result = compareQuantityToNumber(b, a);
55
+ // Flip the comparison result
56
+ if (result.kind === 'less') return { kind: 'greater' };
57
+ if (result.kind === 'greater') return { kind: 'less' };
58
+ return result;
59
+ }
60
+
61
+ // Check if values are complex types (objects/arrays)
62
+ if (isComplex(a) && isComplex(b)) {
63
+ return compareComplex(a, b);
64
+ }
65
+
66
+ // Primitive comparison
67
+ return comparePrimitive(a, b);
68
+ }
69
+
70
+ /**
71
+ * Compare collections (arrays of values)
72
+ * Collections are compared element-by-element in order
73
+ */
74
+ export function compareCollections(left: FHIRPathValue[], right: FHIRPathValue[]): ComparisonResult {
75
+ // Empty collections
76
+ if (left.length === 0 || right.length === 0) {
77
+ return { kind: 'incomparable', reason: 'empty collection' };
78
+ }
79
+
80
+ // Single value collections
81
+ if (left.length === 1 && right.length === 1) {
82
+ const leftItem = left[0];
83
+ const rightItem = right[0];
84
+ if (!leftItem || !rightItem) {
85
+ return { kind: 'incomparable', reason: 'undefined element in collection' };
86
+ }
87
+ const leftValue = unbox(leftItem);
88
+ const rightValue = unbox(rightItem);
89
+ return compare(leftValue, rightValue);
90
+ }
91
+
92
+ // Multiple value collections - must be same length for comparison
93
+ if (left.length !== right.length) {
94
+ // For ordering comparison, different lengths are incomparable
95
+ // But the helper functions handle equality specially
96
+ return { kind: 'incomparable', reason: 'different collection lengths' };
97
+ }
98
+
99
+ // Compare element by element
100
+ for (let i = 0; i < left.length; i++) {
101
+ const leftItem = left[i];
102
+ const rightItem = right[i];
103
+ if (!leftItem || !rightItem) {
104
+ return { kind: 'incomparable', reason: 'undefined element in collection' };
105
+ }
106
+ const leftValue = unbox(leftItem);
107
+ const rightValue = unbox(rightItem);
108
+ const result = compare(leftValue, rightValue);
109
+
110
+ if (result.kind !== 'equal') {
111
+ // First non-equal element determines the result
112
+ return result;
113
+ }
114
+ }
115
+
116
+ return { kind: 'equal' };
117
+ }
118
+
119
+ /**
120
+ * Check if collections are equal
121
+ */
122
+ export function collectionsEqual(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
123
+ // Early exit: reference equality
124
+ if (left === right) {
125
+ return true;
126
+ }
127
+
128
+ // Empty collections return null (incomparable)
129
+ if (left.length === 0 || right.length === 0) {
130
+ return null;
131
+ }
132
+
133
+ // Early exit: different lengths are definitively not equal
134
+ if (left.length !== right.length) {
135
+ return false;
136
+ }
137
+
138
+ // Compare elements
139
+ const result = compareCollections(left, right);
140
+ if (result.kind === 'incomparable') {
141
+ // Special case: if the reason is "complex types not equal", we know they're not equal
142
+ if (result.reason === 'complex types not equal') {
143
+ return false;
144
+ }
145
+ return null;
146
+ }
147
+ return result.kind === 'equal';
148
+ }
149
+
150
+ /**
151
+ * Check if collections are not equal
152
+ */
153
+ export function collectionsNotEqual(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
154
+ // Early exit: reference equality means definitely equal (so not not-equal)
155
+ if (left === right && left.length > 0) {
156
+ return false;
157
+ }
158
+
159
+ // Empty collections return null (incomparable)
160
+ if (left.length === 0 || right.length === 0) {
161
+ return null;
162
+ }
163
+
164
+ // Early exit: different lengths are definitively not equal
165
+ if (left.length !== right.length) {
166
+ return true;
167
+ }
168
+
169
+ // Compare elements
170
+ const result = compareCollections(left, right);
171
+ if (result.kind === 'incomparable') {
172
+ // Special case: if the reason is "complex types not equal", we know they're not equal
173
+ if (result.reason === 'complex types not equal') {
174
+ return true;
175
+ }
176
+ return null;
177
+ }
178
+ return result.kind !== 'equal';
179
+ }
180
+
181
+ // Type guards
182
+ function isTemporalValue(value: unknown): value is TemporalValue {
183
+ if (!value || typeof value !== 'object') return false;
184
+ const v = value as any;
185
+ return isFHIRDate(v) || isFHIRDateTime(v) || isFHIRTime(v);
186
+ }
187
+
188
+ function isQuantity(value: unknown): value is QuantityValue {
189
+ if (!value || typeof value !== 'object') return false;
190
+ const v = value as any;
191
+ return 'unit' in v && 'value' in v && typeof v.value === 'number' && typeof v.unit === 'string';
192
+ }
193
+
194
+ function isComplex(value: unknown): value is object {
195
+ return value !== null && typeof value === 'object' && !isTemporalValue(value) && !isQuantity(value);
196
+ }
197
+
198
+ // Type-specific comparison functions
199
+
200
+ function compareTemporal(a: TemporalValue, b: TemporalValue): ComparisonResult {
201
+ // Use existing temporal comparison logic
202
+ const compareResult = temporalCompare(a, b);
203
+
204
+ if (compareResult === null) {
205
+ return { kind: 'incomparable', reason: 'incomparable temporal values' };
206
+ }
207
+ if (compareResult === 0) {
208
+ return { kind: 'equal' };
209
+ }
210
+ if (compareResult < 0) {
211
+ return { kind: 'less' };
212
+ }
213
+ return { kind: 'greater' };
214
+ }
215
+
216
+ function compareQuantityValues(a: QuantityValue, b: QuantityValue): ComparisonResult {
217
+ const result = compareQuantities(a, b);
218
+
219
+ if (result === null) {
220
+ return { kind: 'incomparable', reason: 'incompatible quantity dimensions' };
221
+ }
222
+ if (result === 0) {
223
+ return { kind: 'equal' };
224
+ }
225
+ if (result < 0) {
226
+ return { kind: 'less' };
227
+ }
228
+ return { kind: 'greater' };
229
+ }
230
+
231
+ function compareQuantityToNumber(quantity: QuantityValue, number: number): ComparisonResult {
232
+ // Dimensionless quantities can be compared to numbers
233
+ if (quantity.unit === '1' || quantity.unit === '') {
234
+ if (quantity.value === number) {
235
+ return { kind: 'equal' };
236
+ }
237
+ if (quantity.value < number) {
238
+ return { kind: 'less' };
239
+ }
240
+ return { kind: 'greater' };
241
+ }
242
+ // Non-dimensionless quantity cannot be compared to a number
243
+ return { kind: 'incomparable', reason: 'cannot compare dimensioned quantity to number' };
244
+ }
245
+
246
+ function compareComplex(a: any, b: any): ComparisonResult {
247
+ // Use deep equality for complex types
248
+ if (deepEqual(a, b)) {
249
+ return { kind: 'equal' };
250
+ }
251
+ // Complex types are not orderable, so we can't say less or greater
252
+ // But for equality purposes, we know they're not equal
253
+ return { kind: 'incomparable', reason: 'complex types not equal' };
254
+ }
255
+
256
+ function comparePrimitive(a: unknown, b: unknown): ComparisonResult {
257
+ // String comparison
258
+ if (typeof a === 'string' && typeof b === 'string') {
259
+ if (a === b) return { kind: 'equal' };
260
+ if (a < b) return { kind: 'less' };
261
+ return { kind: 'greater' };
262
+ }
263
+
264
+ // Number comparison
265
+ if (typeof a === 'number' && typeof b === 'number') {
266
+ if (a === b) return { kind: 'equal' };
267
+ if (a < b) return { kind: 'less' };
268
+ return { kind: 'greater' };
269
+ }
270
+
271
+ // Boolean comparison
272
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
273
+ if (a === b) return { kind: 'equal' };
274
+ // false < true in FHIRPath
275
+ if (!a && b) return { kind: 'less' };
276
+ return { kind: 'greater' };
277
+ }
278
+
279
+ // Type mismatch
280
+ if (typeof a !== typeof b) {
281
+ return { kind: 'incomparable', reason: 'type mismatch' };
282
+ }
283
+
284
+ // Fallback to strict equality
285
+ if (a === b) {
286
+ return { kind: 'equal' };
287
+ }
288
+ return { kind: 'incomparable', reason: 'incomparable values' };
289
+ }
290
+
291
+ /**
292
+ * Deep equality comparison for complex types
293
+ * Handles arrays, objects, and nested structures
294
+ */
295
+ export function deepEqual(a: any, b: any): boolean {
296
+ // Early exit: same reference
297
+ if (a === b) return true;
298
+
299
+ // Early exit: different types
300
+ const typeA = typeof a;
301
+ const typeB = typeof b;
302
+ if (typeA !== typeB) return false;
303
+
304
+ // Early exit: primitives
305
+ if (typeA !== 'object') return false; // We already checked a === b
306
+
307
+ // Null or undefined
308
+ if (a == null || b == null) return a === b;
309
+
310
+ // Arrays
311
+ if (Array.isArray(a) && Array.isArray(b)) {
312
+ if (a.length !== b.length) return false;
313
+ for (let i = 0; i < a.length; i++) {
314
+ if (!deepEqual(a[i], b[i])) return false;
315
+ }
316
+ return true;
317
+ }
318
+
319
+ // Objects
320
+ if (typeof a === 'object' && typeof b === 'object') {
321
+ // Special handling for temporal and quantity types
322
+ if (isTemporalValue(a) && isTemporalValue(b)) {
323
+ return temporalEquals(a, b) === true;
324
+ }
325
+ if (isQuantity(a) && isQuantity(b)) {
326
+ return compareQuantities(a, b) === 0;
327
+ }
328
+
329
+ // General object comparison
330
+ const keysA = Object.keys(a);
331
+ const keysB = Object.keys(b);
332
+
333
+ if (keysA.length !== keysB.length) return false;
334
+
335
+ for (const key of keysA) {
336
+ if (!keysB.includes(key)) return false;
337
+ if (!deepEqual(a[key], b[key])) return false;
338
+ }
339
+ return true;
340
+ }
341
+
342
+ // Primitives
343
+ return a === b;
344
+ }
345
+
346
+ // Performance optimization: Caching for repeated comparisons
347
+ const comparisonCache = new WeakMap<any, WeakMap<any, ComparisonResult>>();
348
+
349
+ /**
350
+ * Compare with caching for performance
351
+ * Uses WeakMap to avoid memory leaks and automatically clean up when objects are garbage collected
352
+ */
353
+ export function compareWithCache(a: unknown, b: unknown): ComparisonResult {
354
+ // Only cache object comparisons (primitives are fast enough)
355
+ if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
356
+ let aCache = comparisonCache.get(a);
357
+ if (aCache) {
358
+ const cached = aCache.get(b);
359
+ if (cached) return cached;
360
+ } else {
361
+ aCache = new WeakMap();
362
+ comparisonCache.set(a, aCache);
363
+ }
364
+
365
+ const result = compare(a, b);
366
+ aCache.set(b, result);
367
+ return result;
368
+ }
369
+
370
+ return compare(a, b);
371
+ }
372
+
373
+ // ============================================================================
374
+ // Equivalence Implementation
375
+ // ============================================================================
376
+
377
+ /**
378
+ * Check if two values are equivalent according to FHIRPath semantics
379
+ * Equivalence is more permissive than equality:
380
+ * - Strings are compared case-insensitively with normalized whitespace
381
+ * - Decimals ignore trailing zeros (2.0 ~ 2.00)
382
+ * - Quantities use UCUM semantic equivalence
383
+ * - Collections are compared without considering order
384
+ * - null/empty are considered equivalent
385
+ */
386
+ export function equivalent(a: unknown, b: unknown): boolean | null {
387
+ // Handle null/empty equivalence
388
+ if (isEmpty(a) && isEmpty(b)) return true;
389
+ if (isEmpty(a) || isEmpty(b)) return false;
390
+
391
+ // String equivalence - case insensitive, normalized whitespace
392
+ if (typeof a === 'string' && typeof b === 'string') {
393
+ return stringEquivalent(a, b);
394
+ }
395
+
396
+ // Number/Decimal equivalence - semantic value comparison
397
+ if (typeof a === 'number' && typeof b === 'number') {
398
+ return decimalEquivalent(a, b);
399
+ }
400
+
401
+ // Quantity equivalence
402
+ if (isQuantity(a) && isQuantity(b)) {
403
+ return quantityEquivalent(a, b);
404
+ }
405
+
406
+ // Temporal types use equality semantics
407
+ // But for equivalence, incomparable (null) means not equivalent (false)
408
+ if (isTemporalValue(a) && isTemporalValue(b)) {
409
+ const result = temporalEquals(a, b);
410
+ // If temporal values are incomparable (different precision), they're not equivalent
411
+ return result === null ? false : result;
412
+ }
413
+
414
+ // Boolean equivalence is same as equality
415
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
416
+ return a === b;
417
+ }
418
+
419
+ // Complex types need deep equivalence
420
+ if (isComplex(a) && isComplex(b)) {
421
+ return deepEquivalent(a, b);
422
+ }
423
+
424
+ // Type mismatch
425
+ if (typeof a !== typeof b) {
426
+ return false;
427
+ }
428
+
429
+ // Default to strict equality
430
+ return a === b;
431
+ }
432
+
433
+ /**
434
+ * Check if a value is empty (null, undefined, or empty array/object)
435
+ */
436
+ function isEmpty(value: unknown): boolean {
437
+ if (value === null || value === undefined) return true;
438
+ if (Array.isArray(value) && value.length === 0) return true;
439
+ if (typeof value === 'object' && Object.keys(value).length === 0) return true;
440
+ return false;
441
+ }
442
+
443
+ /**
444
+ * String equivalence with case-insensitive comparison and whitespace normalization
445
+ */
446
+ function stringEquivalent(a: string, b: string): boolean {
447
+ // Normalize whitespace: collapse multiple spaces, trim ends
448
+ const normalize = (s: string) =>
449
+ s.replace(/\s+/g, ' ').trim().toLowerCase();
450
+
451
+ return normalize(a) === normalize(b);
452
+ }
453
+
454
+ /**
455
+ * Decimal equivalence for FHIRPath
456
+ *
457
+ * Per spec: "comparison is done on values rounded to the precision of the
458
+ * least precise operand. Trailing zeroes after the decimal are ignored in
459
+ * determining precision."
460
+ *
461
+ * Since JavaScript loses literal precision (1.0 becomes 1), we deduce precision:
462
+ * - Numbers with no fractional part (1, 2.0 -> 2) have 0 decimal places
463
+ * - Numbers with fractional parts use their actual decimal places
464
+ */
465
+ function decimalEquivalent(a: number, b: number): boolean {
466
+ // Handle special cases
467
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
468
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
469
+
470
+ // Infinite values must match exactly
471
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
472
+ return a === b;
473
+ }
474
+
475
+ // Deduce precision from the numeric values
476
+ const aPrecision = getDecimalPrecision(a);
477
+ const bPrecision = getDecimalPrecision(b);
478
+
479
+ // Round both to the minimum precision
480
+ const minPrecision = Math.min(aPrecision, bPrecision);
481
+
482
+ // Round both numbers to the minimum precision
483
+ const factor = Math.pow(10, minPrecision);
484
+ const aRounded = Math.round(a * factor) / factor;
485
+ const bRounded = Math.round(b * factor) / factor;
486
+
487
+ return aRounded === bRounded;
488
+ }
489
+
490
+ /**
491
+ * Get the effective decimal precision of a number.
492
+ * Numbers with no fractional part (like 1.0 which becomes 1) have 0 precision.
493
+ * Numbers with fractional parts have precision based on significant decimal places.
494
+ */
495
+ function getDecimalPrecision(n: number): number {
496
+ // If it's effectively an integer (no fractional part), precision is 0
497
+ if (Number.isInteger(n)) {
498
+ return 0;
499
+ }
500
+
501
+ // Convert to string to count decimal places
502
+ // Use a reasonable maximum precision to avoid floating point artifacts
503
+ const str = n.toFixed(8).replace(/0+$/, '').replace(/\.$/, '');
504
+ const decimalIndex = str.indexOf('.');
505
+
506
+ if (decimalIndex === -1) {
507
+ return 0;
508
+ }
509
+
510
+ return str.length - decimalIndex - 1;
511
+ }
512
+
513
+ // Calendar to UCUM duration mappings for equivalence
514
+ const CALENDAR_TO_UCUM_MAP: Record<string, string> = {
515
+ 'year': 'a',
516
+ 'years': 'a',
517
+ 'month': 'mo',
518
+ 'months': 'mo',
519
+ 'week': 'wk',
520
+ 'weeks': 'wk',
521
+ 'day': 'd',
522
+ 'days': 'd',
523
+ 'hour': 'h',
524
+ 'hours': 'h',
525
+ 'minute': 'min',
526
+ 'minutes': 'min',
527
+ 'second': 's',
528
+ 'seconds': 's',
529
+ 'millisecond': 'ms',
530
+ 'milliseconds': 'ms'
531
+ };
532
+
533
+ /**
534
+ * Quantity equivalence with UCUM semantic comparison and calendar mappings
535
+ */
536
+ function quantityEquivalent(a: QuantityValue, b: QuantityValue): boolean | null {
537
+ // Check for calendar to UCUM equivalence
538
+ const aIsCalendar = CALENDAR_TO_UCUM_MAP[a.unit] !== undefined;
539
+ const bIsCalendar = CALENDAR_TO_UCUM_MAP[b.unit] !== undefined;
540
+
541
+ // Calendar to UCUM mapping
542
+ if (aIsCalendar && !bIsCalendar) {
543
+ const ucumUnit = CALENDAR_TO_UCUM_MAP[a.unit];
544
+ return ucumUnit === b.unit && a.value === b.value;
545
+ }
546
+ if (!aIsCalendar && bIsCalendar) {
547
+ const ucumUnit = CALENDAR_TO_UCUM_MAP[b.unit];
548
+ return ucumUnit === a.unit && a.value === b.value;
549
+ }
550
+
551
+ // Both calendar units - must be same unit and value
552
+ if (aIsCalendar && bIsCalendar) {
553
+ // Normalize to singular/plural
554
+ const aNorm = CALENDAR_TO_UCUM_MAP[a.unit];
555
+ const bNorm = CALENDAR_TO_UCUM_MAP[b.unit];
556
+ return aNorm === bNorm && a.value === b.value;
557
+ }
558
+
559
+ // Standard UCUM semantic equivalence (1000 mg ~ 1 g)
560
+ const result = compareQuantities(a, b);
561
+ return result === 0;
562
+ }
563
+
564
+ /**
565
+ * Deep equivalence for complex objects
566
+ * Similar to deep equality but uses equivalence rules for nested values
567
+ */
568
+ function deepEquivalent(a: any, b: any): boolean {
569
+ // Early exit: same reference
570
+ if (a === b) return true;
571
+
572
+ // Arrays - compare elements using equivalence
573
+ if (Array.isArray(a) && Array.isArray(b)) {
574
+ if (a.length !== b.length) return false;
575
+ for (let i = 0; i < a.length; i++) {
576
+ const equiv = equivalent(a[i], b[i]);
577
+ if (equiv !== true) return false;
578
+ }
579
+ return true;
580
+ }
581
+
582
+ // Objects - compare properties using equivalence
583
+ if (typeof a === 'object' && typeof b === 'object') {
584
+ const keysA = Object.keys(a);
585
+ const keysB = Object.keys(b);
586
+
587
+ if (keysA.length !== keysB.length) return false;
588
+
589
+ for (const key of keysA) {
590
+ if (!keysB.includes(key)) return false;
591
+ const equiv = equivalent(a[key], b[key]);
592
+ if (equiv !== true) return false;
593
+ }
594
+ return true;
595
+ }
596
+
597
+ return false;
598
+ }
599
+
600
+ /**
601
+ * Compare collections for equivalence
602
+ * Collections are equivalent if they contain the same elements regardless of order
603
+ */
604
+ export function collectionsEquivalent(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
605
+ // Empty collections are equivalent
606
+ if (left.length === 0 && right.length === 0) return true;
607
+
608
+ // One empty, one not - not equivalent
609
+ if (left.length === 0 || right.length === 0) return false;
610
+
611
+ // Different lengths = not equivalent
612
+ if (left.length !== right.length) return false;
613
+
614
+ // Single element collections
615
+ if (left.length === 1 && right.length === 1) {
616
+ const leftValue = unbox(left[0]!);
617
+ const rightValue = unbox(right[0]!);
618
+ return equivalent(leftValue, rightValue);
619
+ }
620
+
621
+ // Sort both collections for comparison
622
+ // We need a stable sort that groups equivalent elements together
623
+ const sortedLeft = [...left].sort((a, b) => sortCompareForEquivalence(unbox(a), unbox(b)));
624
+ const sortedRight = [...right].sort((a, b) => sortCompareForEquivalence(unbox(a), unbox(b)));
625
+
626
+ // Compare sorted elements using equivalence
627
+ for (let i = 0; i < sortedLeft.length; i++) {
628
+ const leftValue = unbox(sortedLeft[i]!);
629
+ const rightValue = unbox(sortedRight[i]!);
630
+ const equiv = equivalent(leftValue, rightValue);
631
+
632
+ if (equiv === null) return null;
633
+ if (equiv === false) return false;
634
+ }
635
+
636
+ return true;
637
+ }
638
+
639
+ /**
640
+ * Compare collections for non-equivalence
641
+ */
642
+ export function collectionsNotEquivalent(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
643
+ const result = collectionsEquivalent(left, right);
644
+ if (result === null) return null;
645
+ return !result;
646
+ }
647
+
648
+ /**
649
+ * Comparison function for sorting collections for equivalence comparison
650
+ * Groups equivalent items together
651
+ */
652
+ function sortCompareForEquivalence(a: unknown, b: unknown): number {
653
+ // Handle null/undefined
654
+ if (a === null || a === undefined) return b === null || b === undefined ? 0 : -1;
655
+ if (b === null || b === undefined) return 1;
656
+
657
+ // Type-based ordering
658
+ const typeOrder = ['boolean', 'number', 'string', 'object'];
659
+ const typeA = typeof a;
660
+ const typeB = typeof b;
661
+ const orderA = typeOrder.indexOf(typeA);
662
+ const orderB = typeOrder.indexOf(typeB);
663
+
664
+ if (orderA !== orderB) {
665
+ return orderA - orderB;
666
+ }
667
+
668
+ // Same type comparison
669
+ switch (typeA) {
670
+ case 'boolean':
671
+ return (a as boolean) === (b as boolean) ? 0 : (a as boolean) ? 1 : -1;
672
+
673
+ case 'number':
674
+ return (a as number) - (b as number);
675
+
676
+ case 'string':
677
+ // Use normalized comparison for sorting
678
+ const aNorm = (a as string).toLowerCase().trim();
679
+ const bNorm = (b as string).toLowerCase().trim();
680
+ return aNorm < bNorm ? -1 : aNorm > bNorm ? 1 : 0;
681
+
682
+ case 'object':
683
+ // For objects, use a stable stringification
684
+ // This is not perfect but works for sorting purposes
685
+ const aStr = JSON.stringify(a);
686
+ const bStr = JSON.stringify(b);
687
+ return aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
688
+
689
+ default:
690
+ return 0;
691
+ }
692
+ }
@@ -1,7 +1,7 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
3
  import type { FunctionEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
7
  // contains() requires exactly 1 argument