@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,744 @@
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
+ // Special case: Date vs Time are definitively not equal
146
+ // Per XML tests: Date != Time returns true, Date = Time returns false
147
+ if (result.reason === 'date vs time') {
148
+ return false;
149
+ }
150
+ // Other incomparable cases (e.g., Date vs DateTime with same date) remain incomparable
151
+ return null;
152
+ }
153
+ return result.kind === 'equal';
154
+ }
155
+
156
+ /**
157
+ * Check if collections are not equal
158
+ */
159
+ export function collectionsNotEqual(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
160
+ // Early exit: reference equality means definitely equal (so not not-equal)
161
+ if (left === right && left.length > 0) {
162
+ return false;
163
+ }
164
+
165
+ // Empty collections return null (incomparable)
166
+ if (left.length === 0 || right.length === 0) {
167
+ return null;
168
+ }
169
+
170
+ // Early exit: different lengths are definitively not equal
171
+ if (left.length !== right.length) {
172
+ return true;
173
+ }
174
+
175
+ // Compare elements
176
+ const result = compareCollections(left, right);
177
+ if (result.kind === 'incomparable') {
178
+ // Special case: if the reason is "complex types not equal", we know they're not equal
179
+ if (result.reason === 'complex types not equal') {
180
+ return true;
181
+ }
182
+ // Special case: Date vs Time are definitively not equal
183
+ // Per XML tests: Date != Time returns true, Date = Time returns false
184
+ if (result.reason === 'date vs time') {
185
+ return true;
186
+ }
187
+ // Other incomparable cases (e.g., Date vs DateTime with same date) remain incomparable
188
+ return null;
189
+ }
190
+ return result.kind !== 'equal';
191
+ }
192
+
193
+ // Type guards
194
+ function isTemporalValue(value: unknown): value is TemporalValue {
195
+ if (!value || typeof value !== 'object') return false;
196
+ const v = value as any;
197
+ return isFHIRDate(v) || isFHIRDateTime(v) || isFHIRTime(v);
198
+ }
199
+
200
+ function isQuantity(value: unknown): value is QuantityValue {
201
+ if (!value || typeof value !== 'object') return false;
202
+ const v = value as any;
203
+ // Check for FHIRPath quantity (has unit and value)
204
+ if ('unit' in v && 'value' in v && typeof v.value === 'number' && typeof v.unit === 'string') {
205
+ return true;
206
+ }
207
+ // Check for FHIR Quantity (has code or unit, and value)
208
+ if ('value' in v && typeof v.value === 'number' && ('code' in v || 'unit' in v)) {
209
+ return true;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ function isComplex(value: unknown): value is object {
215
+ return value !== null && typeof value === 'object' && !isTemporalValue(value) && !isQuantity(value);
216
+ }
217
+
218
+ // Type-specific comparison functions
219
+
220
+ function compareTemporal(a: TemporalValue, b: TemporalValue): ComparisonResult {
221
+ // Check for Date vs Time - these are definitively not equal
222
+ if ((isFHIRDate(a) && isFHIRTime(b)) || (isFHIRTime(a) && isFHIRDate(b))) {
223
+ // Date and Time are completely different types - not equal, not incomparable
224
+ return { kind: 'incomparable', reason: 'date vs time' };
225
+ }
226
+
227
+ // Use existing temporal comparison logic
228
+ const compareResult = temporalCompare(a, b);
229
+
230
+ if (compareResult === null) {
231
+ return { kind: 'incomparable', reason: 'incomparable temporal values' };
232
+ }
233
+ if (compareResult === 0) {
234
+ return { kind: 'equal' };
235
+ }
236
+ if (compareResult < 0) {
237
+ return { kind: 'less' };
238
+ }
239
+ return { kind: 'greater' };
240
+ }
241
+
242
+ function normalizeQuantity(q: any): QuantityValue {
243
+ // For FHIR Quantity, use code field if present, otherwise unit
244
+ if ('code' in q && typeof q.code === 'string') {
245
+ return { value: q.value, unit: q.code };
246
+ }
247
+ return { value: q.value, unit: q.unit };
248
+ }
249
+
250
+ function compareQuantityValues(a: QuantityValue, b: QuantityValue): ComparisonResult {
251
+ // Normalize both quantities to handle FHIR vs FHIRPath quantities
252
+ const normA = normalizeQuantity(a);
253
+ const normB = normalizeQuantity(b);
254
+
255
+ const result = compareQuantities(normA, normB);
256
+
257
+ if (result === null) {
258
+ return { kind: 'incomparable', reason: 'incompatible quantity dimensions' };
259
+ }
260
+ if (result === 0) {
261
+ return { kind: 'equal' };
262
+ }
263
+ if (result < 0) {
264
+ return { kind: 'less' };
265
+ }
266
+ return { kind: 'greater' };
267
+ }
268
+
269
+ function compareQuantityToNumber(quantity: QuantityValue, number: number): ComparisonResult {
270
+ // Dimensionless quantities can be compared to numbers
271
+ if (quantity.unit === '1' || quantity.unit === '') {
272
+ if (quantity.value === number) {
273
+ return { kind: 'equal' };
274
+ }
275
+ if (quantity.value < number) {
276
+ return { kind: 'less' };
277
+ }
278
+ return { kind: 'greater' };
279
+ }
280
+ // Non-dimensionless quantity cannot be compared to a number
281
+ return { kind: 'incomparable', reason: 'cannot compare dimensioned quantity to number' };
282
+ }
283
+
284
+ function compareComplex(a: any, b: any): ComparisonResult {
285
+ // Use deep equality for complex types
286
+ if (deepEqual(a, b)) {
287
+ return { kind: 'equal' };
288
+ }
289
+ // Complex types are not orderable, so we can't say less or greater
290
+ // But for equality purposes, we know they're not equal
291
+ return { kind: 'incomparable', reason: 'complex types not equal' };
292
+ }
293
+
294
+ function comparePrimitive(a: unknown, b: unknown): ComparisonResult {
295
+ // String comparison
296
+ if (typeof a === 'string' && typeof b === 'string') {
297
+ if (a === b) return { kind: 'equal' };
298
+ if (a < b) return { kind: 'less' };
299
+ return { kind: 'greater' };
300
+ }
301
+
302
+ // Number comparison
303
+ if (typeof a === 'number' && typeof b === 'number') {
304
+ if (a === b) return { kind: 'equal' };
305
+ if (a < b) return { kind: 'less' };
306
+ return { kind: 'greater' };
307
+ }
308
+
309
+ // Boolean comparison
310
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
311
+ if (a === b) return { kind: 'equal' };
312
+ // false < true in FHIRPath
313
+ if (!a && b) return { kind: 'less' };
314
+ return { kind: 'greater' };
315
+ }
316
+
317
+ // Type mismatch
318
+ if (typeof a !== typeof b) {
319
+ return { kind: 'incomparable', reason: 'type mismatch' };
320
+ }
321
+
322
+ // Fallback to strict equality
323
+ if (a === b) {
324
+ return { kind: 'equal' };
325
+ }
326
+ return { kind: 'incomparable', reason: 'incomparable values' };
327
+ }
328
+
329
+ /**
330
+ * Deep equality comparison for complex types
331
+ * Handles arrays, objects, and nested structures
332
+ */
333
+ export function deepEqual(a: any, b: any): boolean {
334
+ // Early exit: same reference
335
+ if (a === b) return true;
336
+
337
+ // Early exit: different types
338
+ const typeA = typeof a;
339
+ const typeB = typeof b;
340
+ if (typeA !== typeB) return false;
341
+
342
+ // Early exit: primitives
343
+ if (typeA !== 'object') return false; // We already checked a === b
344
+
345
+ // Null or undefined
346
+ if (a == null || b == null) return a === b;
347
+
348
+ // Arrays
349
+ if (Array.isArray(a) && Array.isArray(b)) {
350
+ if (a.length !== b.length) return false;
351
+ for (let i = 0; i < a.length; i++) {
352
+ if (!deepEqual(a[i], b[i])) return false;
353
+ }
354
+ return true;
355
+ }
356
+
357
+ // Objects
358
+ if (typeof a === 'object' && typeof b === 'object') {
359
+ // Special handling for temporal and quantity types
360
+ if (isTemporalValue(a) && isTemporalValue(b)) {
361
+ return temporalEquals(a, b) === true;
362
+ }
363
+ if (isQuantity(a) && isQuantity(b)) {
364
+ return compareQuantities(a, b) === 0;
365
+ }
366
+
367
+ // General object comparison
368
+ const keysA = Object.keys(a);
369
+ const keysB = Object.keys(b);
370
+
371
+ if (keysA.length !== keysB.length) return false;
372
+
373
+ for (const key of keysA) {
374
+ if (!keysB.includes(key)) return false;
375
+ if (!deepEqual(a[key], b[key])) return false;
376
+ }
377
+ return true;
378
+ }
379
+
380
+ // Primitives
381
+ return a === b;
382
+ }
383
+
384
+ // Performance optimization: Caching for repeated comparisons
385
+ const comparisonCache = new WeakMap<any, WeakMap<any, ComparisonResult>>();
386
+
387
+ /**
388
+ * Compare with caching for performance
389
+ * Uses WeakMap to avoid memory leaks and automatically clean up when objects are garbage collected
390
+ */
391
+ export function compareWithCache(a: unknown, b: unknown): ComparisonResult {
392
+ // Only cache object comparisons (primitives are fast enough)
393
+ if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
394
+ let aCache = comparisonCache.get(a);
395
+ if (aCache) {
396
+ const cached = aCache.get(b);
397
+ if (cached) return cached;
398
+ } else {
399
+ aCache = new WeakMap();
400
+ comparisonCache.set(a, aCache);
401
+ }
402
+
403
+ const result = compare(a, b);
404
+ aCache.set(b, result);
405
+ return result;
406
+ }
407
+
408
+ return compare(a, b);
409
+ }
410
+
411
+ // ============================================================================
412
+ // Equivalence Implementation
413
+ // ============================================================================
414
+
415
+ /**
416
+ * Check if two values are equivalent according to FHIRPath semantics
417
+ * Equivalence is more permissive than equality:
418
+ * - Strings are compared case-insensitively with normalized whitespace
419
+ * - Decimals ignore trailing zeros (2.0 ~ 2.00)
420
+ * - Quantities use UCUM semantic equivalence
421
+ * - Collections are compared without considering order
422
+ * - null/empty are considered equivalent
423
+ */
424
+ export function equivalent(a: unknown, b: unknown): boolean | null {
425
+ // Handle null/empty equivalence
426
+ if (isEmpty(a) && isEmpty(b)) return true;
427
+ if (isEmpty(a) || isEmpty(b)) return false;
428
+
429
+ // String equivalence - case insensitive, normalized whitespace
430
+ if (typeof a === 'string' && typeof b === 'string') {
431
+ return stringEquivalent(a, b);
432
+ }
433
+
434
+ // Number/Decimal equivalence - semantic value comparison
435
+ if (typeof a === 'number' && typeof b === 'number') {
436
+ return decimalEquivalent(a, b);
437
+ }
438
+
439
+ // Quantity equivalence
440
+ if (isQuantity(a) && isQuantity(b)) {
441
+ return quantityEquivalent(a, b);
442
+ }
443
+
444
+ // Temporal types use equality semantics
445
+ // But for equivalence, incomparable (null) means not equivalent (false)
446
+ if (isTemporalValue(a) && isTemporalValue(b)) {
447
+ const result = temporalEquals(a, b);
448
+ // If temporal values are incomparable (different precision), they're not equivalent
449
+ return result === null ? false : result;
450
+ }
451
+
452
+ // Boolean equivalence is same as equality
453
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
454
+ return a === b;
455
+ }
456
+
457
+ // Complex types need deep equivalence
458
+ if (isComplex(a) && isComplex(b)) {
459
+ return deepEquivalent(a, b);
460
+ }
461
+
462
+ // Type mismatch
463
+ if (typeof a !== typeof b) {
464
+ return false;
465
+ }
466
+
467
+ // Default to strict equality
468
+ return a === b;
469
+ }
470
+
471
+ /**
472
+ * Check if a value is empty (null, undefined, or empty array/object)
473
+ */
474
+ function isEmpty(value: unknown): boolean {
475
+ if (value === null || value === undefined) return true;
476
+ if (Array.isArray(value) && value.length === 0) return true;
477
+ if (typeof value === 'object' && Object.keys(value).length === 0) return true;
478
+ return false;
479
+ }
480
+
481
+ /**
482
+ * String equivalence with case-insensitive comparison and whitespace normalization
483
+ */
484
+ function stringEquivalent(a: string, b: string): boolean {
485
+ // Normalize whitespace: collapse multiple spaces, trim ends
486
+ const normalize = (s: string) =>
487
+ s.replace(/\s+/g, ' ').trim().toLowerCase();
488
+
489
+ return normalize(a) === normalize(b);
490
+ }
491
+
492
+ /**
493
+ * Decimal equivalence for FHIRPath
494
+ *
495
+ * Per spec: "comparison is done on values rounded to the precision of the
496
+ * least precise operand. Trailing zeroes after the decimal are ignored in
497
+ * determining precision."
498
+ *
499
+ * Since JavaScript loses literal precision (1.0 becomes 1), we deduce precision:
500
+ * - Numbers with no fractional part (1, 2.0 -> 2) have 0 decimal places
501
+ * - Numbers with fractional parts use their actual decimal places
502
+ */
503
+ function decimalEquivalent(a: number, b: number): boolean {
504
+ // Handle special cases
505
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
506
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
507
+
508
+ // Infinite values must match exactly
509
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
510
+ return a === b;
511
+ }
512
+
513
+ // Deduce precision from the numeric values
514
+ const aPrecision = getDecimalPrecision(a);
515
+ const bPrecision = getDecimalPrecision(b);
516
+
517
+ // Round both to the minimum precision
518
+ const minPrecision = Math.min(aPrecision, bPrecision);
519
+
520
+ // Round both numbers to the minimum precision
521
+ const factor = Math.pow(10, minPrecision);
522
+ const aRounded = Math.round(a * factor) / factor;
523
+ const bRounded = Math.round(b * factor) / factor;
524
+
525
+ return aRounded === bRounded;
526
+ }
527
+
528
+ /**
529
+ * Get the effective decimal precision of a number.
530
+ * Numbers with no fractional part (like 1.0 which becomes 1) have 0 precision.
531
+ * Numbers with fractional parts have precision based on significant decimal places.
532
+ */
533
+ function getDecimalPrecision(n: number): number {
534
+ // If it's effectively an integer (no fractional part), precision is 0
535
+ if (Number.isInteger(n)) {
536
+ return 0;
537
+ }
538
+
539
+ // Convert to string to count decimal places
540
+ // Use a reasonable maximum precision to avoid floating point artifacts
541
+ const str = n.toFixed(8).replace(/0+$/, '').replace(/\.$/, '');
542
+ const decimalIndex = str.indexOf('.');
543
+
544
+ if (decimalIndex === -1) {
545
+ return 0;
546
+ }
547
+
548
+ return str.length - decimalIndex - 1;
549
+ }
550
+
551
+ // Calendar to UCUM duration mappings for equivalence
552
+ const CALENDAR_TO_UCUM_MAP: Record<string, string> = {
553
+ 'year': 'a',
554
+ 'years': 'a',
555
+ 'month': 'mo',
556
+ 'months': 'mo',
557
+ 'week': 'wk',
558
+ 'weeks': 'wk',
559
+ 'day': 'd',
560
+ 'days': 'd',
561
+ 'hour': 'h',
562
+ 'hours': 'h',
563
+ 'minute': 'min',
564
+ 'minutes': 'min',
565
+ 'second': 's',
566
+ 'seconds': 's',
567
+ 'millisecond': 'ms',
568
+ 'milliseconds': 'ms'
569
+ };
570
+
571
+ /**
572
+ * Quantity equivalence with UCUM semantic comparison and calendar mappings
573
+ */
574
+ function quantityEquivalent(a: QuantityValue, b: QuantityValue): boolean | null {
575
+ // Normalize both quantities to handle FHIR vs FHIRPath quantities
576
+ const normA = normalizeQuantity(a);
577
+ const normB = normalizeQuantity(b);
578
+
579
+ // Use compareQuantities which now handles calendar unit conversions
580
+ const result = compareQuantities(normA, normB);
581
+
582
+ // Null means incomparable
583
+ if (result === null) {
584
+ return null;
585
+ }
586
+
587
+ // For equivalence, we check if they're equal when converted
588
+ // compareQuantities returns 0 for equal values
589
+ if (result === 0) {
590
+ return true;
591
+ }
592
+
593
+ // For approximate equivalence (~), check if values are within tolerance
594
+ // This is primarily for handling precision differences like 4 g ~ 4.040 g
595
+ // We need special handling for common unit conversions with tolerance
596
+
597
+ // Check if one is g and other is mg
598
+ if ((a.unit === 'g' && b.unit === 'mg') || (a.unit === 'mg' && b.unit === 'g')) {
599
+ // Convert both to mg for comparison
600
+ const aInMg = a.unit === 'g' ? a.value * 1000 : a.value;
601
+ const bInMg = b.unit === 'g' ? b.value * 1000 : b.value;
602
+
603
+ // Check if within 1% tolerance for approximate equivalence
604
+ const diff = Math.abs(aInMg - bInMg);
605
+ const avg = (aInMg + bInMg) / 2;
606
+ const tolerance = avg * 0.01; // 1% tolerance
607
+
608
+ return diff <= tolerance;
609
+ }
610
+
611
+ // For other units, compareQuantities handles the conversion
612
+ // If it returned non-zero, they're not equivalent
613
+ return false;
614
+ }
615
+
616
+ /**
617
+ * Deep equivalence for complex objects
618
+ * Similar to deep equality but uses equivalence rules for nested values
619
+ */
620
+ function deepEquivalent(a: any, b: any): boolean {
621
+ // Early exit: same reference
622
+ if (a === b) return true;
623
+
624
+ // Arrays - compare elements using equivalence
625
+ if (Array.isArray(a) && Array.isArray(b)) {
626
+ if (a.length !== b.length) return false;
627
+ for (let i = 0; i < a.length; i++) {
628
+ const equiv = equivalent(a[i], b[i]);
629
+ if (equiv !== true) return false;
630
+ }
631
+ return true;
632
+ }
633
+
634
+ // Objects - compare properties using equivalence
635
+ if (typeof a === 'object' && typeof b === 'object') {
636
+ const keysA = Object.keys(a);
637
+ const keysB = Object.keys(b);
638
+
639
+ if (keysA.length !== keysB.length) return false;
640
+
641
+ for (const key of keysA) {
642
+ if (!keysB.includes(key)) return false;
643
+ const equiv = equivalent(a[key], b[key]);
644
+ if (equiv !== true) return false;
645
+ }
646
+ return true;
647
+ }
648
+
649
+ return false;
650
+ }
651
+
652
+ /**
653
+ * Compare collections for equivalence
654
+ * Collections are equivalent if they contain the same elements regardless of order
655
+ */
656
+ export function collectionsEquivalent(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
657
+ // Empty collections are equivalent
658
+ if (left.length === 0 && right.length === 0) return true;
659
+
660
+ // One empty, one not - not equivalent
661
+ if (left.length === 0 || right.length === 0) return false;
662
+
663
+ // Different lengths = not equivalent
664
+ if (left.length !== right.length) return false;
665
+
666
+ // Single element collections
667
+ if (left.length === 1 && right.length === 1) {
668
+ const leftValue = unbox(left[0]!);
669
+ const rightValue = unbox(right[0]!);
670
+ return equivalent(leftValue, rightValue);
671
+ }
672
+
673
+ // Sort both collections for comparison
674
+ // We need a stable sort that groups equivalent elements together
675
+ const sortedLeft = [...left].sort((a, b) => sortCompareForEquivalence(unbox(a), unbox(b)));
676
+ const sortedRight = [...right].sort((a, b) => sortCompareForEquivalence(unbox(a), unbox(b)));
677
+
678
+ // Compare sorted elements using equivalence
679
+ for (let i = 0; i < sortedLeft.length; i++) {
680
+ const leftValue = unbox(sortedLeft[i]!);
681
+ const rightValue = unbox(sortedRight[i]!);
682
+ const equiv = equivalent(leftValue, rightValue);
683
+
684
+ if (equiv === null) return null;
685
+ if (equiv === false) return false;
686
+ }
687
+
688
+ return true;
689
+ }
690
+
691
+ /**
692
+ * Compare collections for non-equivalence
693
+ */
694
+ export function collectionsNotEquivalent(left: FHIRPathValue[], right: FHIRPathValue[]): boolean | null {
695
+ const result = collectionsEquivalent(left, right);
696
+ if (result === null) return null;
697
+ return !result;
698
+ }
699
+
700
+ /**
701
+ * Comparison function for sorting collections for equivalence comparison
702
+ * Groups equivalent items together
703
+ */
704
+ function sortCompareForEquivalence(a: unknown, b: unknown): number {
705
+ // Handle null/undefined
706
+ if (a === null || a === undefined) return b === null || b === undefined ? 0 : -1;
707
+ if (b === null || b === undefined) return 1;
708
+
709
+ // Type-based ordering
710
+ const typeOrder = ['boolean', 'number', 'string', 'object'];
711
+ const typeA = typeof a;
712
+ const typeB = typeof b;
713
+ const orderA = typeOrder.indexOf(typeA);
714
+ const orderB = typeOrder.indexOf(typeB);
715
+
716
+ if (orderA !== orderB) {
717
+ return orderA - orderB;
718
+ }
719
+
720
+ // Same type comparison
721
+ switch (typeA) {
722
+ case 'boolean':
723
+ return (a as boolean) === (b as boolean) ? 0 : (a as boolean) ? 1 : -1;
724
+
725
+ case 'number':
726
+ return (a as number) - (b as number);
727
+
728
+ case 'string':
729
+ // Use normalized comparison for sorting
730
+ const aNorm = (a as string).toLowerCase().trim();
731
+ const bNorm = (b as string).toLowerCase().trim();
732
+ return aNorm < bNorm ? -1 : aNorm > bNorm ? 1 : 0;
733
+
734
+ case 'object':
735
+ // For objects, use a stable stringification
736
+ // This is not perfect but works for sorting purposes
737
+ const aStr = JSON.stringify(a);
738
+ const bStr = JSON.stringify(b);
739
+ return aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
740
+
741
+ default:
742
+ return 0;
743
+ }
744
+ }