@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,96 @@
1
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+
5
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
+ // Check single item in input
7
+ if (input.length === 0) {
8
+ return { value: [], context };
9
+ }
10
+
11
+ if (input.length > 1) {
12
+ throw Errors.stringSingletonRequired('matchesFull', input.length);
13
+ }
14
+
15
+ const boxedInputValue = input[0];
16
+ if (!boxedInputValue) {
17
+ return { value: [], context };
18
+ }
19
+
20
+ const inputValue = unbox(boxedInputValue);
21
+ if (typeof inputValue !== 'string') {
22
+ throw Errors.stringOperationOnNonString('matchesFull');
23
+ }
24
+
25
+ // Check arguments
26
+ if (args.length !== 1) {
27
+ throw Errors.wrongArgumentCount('matchesFull', 1, args.length);
28
+ }
29
+
30
+ // Evaluate regex argument
31
+ const regexArg = args[0];
32
+ if (!regexArg) {
33
+ throw Errors.argumentRequired('matchesFull', 'regex argument');
34
+ }
35
+
36
+ const regexResult = await evaluator(regexArg, input, context);
37
+
38
+ if (regexResult.value.length === 0) {
39
+ return { value: [], context };
40
+ }
41
+
42
+ if (regexResult.value.length > 1) {
43
+ throw Errors.singletonRequired('matchesFull regex', regexResult.value.length);
44
+ }
45
+
46
+ const boxedRegex = regexResult.value[0];
47
+ if (!boxedRegex) {
48
+ return { value: [], context };
49
+ }
50
+
51
+ const regexPattern = unbox(boxedRegex);
52
+ if (typeof regexPattern !== 'string') {
53
+ throw Errors.invalidStringOperation('matchesFull', 'regex argument');
54
+ }
55
+
56
+ try {
57
+ // matchesFull implicitly adds ^ and $ anchors to match the entire string
58
+ // Only add anchors if they're not already present
59
+ let fullPattern = regexPattern;
60
+ if (!regexPattern.startsWith('^')) {
61
+ fullPattern = '^' + fullPattern;
62
+ }
63
+ if (!regexPattern.endsWith('$')) {
64
+ fullPattern = fullPattern + '$';
65
+ }
66
+
67
+ // Create regex with unicode support and single line mode (dotAll)
68
+ // Per spec: case-sensitive, single line mode, allow Unicode
69
+ const regex = new RegExp(fullPattern, 'us');
70
+ const result = regex.test(inputValue);
71
+
72
+ return { value: [box(result, { type: 'Boolean', singleton: true })], context };
73
+ } catch (error) {
74
+ throw new Error(`Invalid regular expression in matchesFull(): ${(error as Error).message}`);
75
+ }
76
+ };
77
+
78
+ export const matchesFullFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
79
+ name: 'matchesFull',
80
+ category: ['string'],
81
+ description: 'Returns true when the input string completely matches the given regular expression',
82
+ examples: [
83
+ "'Library'.matchesFull('Library')",
84
+ "'http://example.org/Library'.matchesFull('Library')",
85
+ "'N8000123'.matchesFull('N[0-9]{7}')"
86
+ ],
87
+ signatures: [{
88
+ name: 'matchesFull',
89
+ input: { type: 'String', singleton: true },
90
+ parameters: [
91
+ { name: 'regex', type: { type: 'String', singleton: true } }
92
+ ],
93
+ result: { type: 'Boolean', singleton: true }
94
+ }],
95
+ evaluate
96
+ };
@@ -0,0 +1,66 @@
1
+ // millisecondOf() function - Extracts millisecond component from Time or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRTime, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const millisecondOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // millisecondOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('millisecondOf', 0, args.length);
11
+ }
12
+
13
+ // Empty input returns empty
14
+ if (input.length === 0) {
15
+ return { value: [], context };
16
+ }
17
+
18
+ // Multiple items throws error
19
+ if (input.length > 1) {
20
+ throw Errors.singletonRequired('millisecondOf', input.length);
21
+ }
22
+
23
+ const boxedValue = input[0];
24
+ if (!boxedValue) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const value = unbox(boxedValue);
29
+
30
+ // Check if it's a Time or DateTime
31
+ if (isFHIRTime(value) || isFHIRDateTime(value)) {
32
+ // Check if millisecond component is present
33
+ if (value.millisecond === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the millisecond as an Integer (0-999)
38
+ return {
39
+ value: [box(value.millisecond, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Time or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const millisecondOfFunction: FunctionDefinition & { evaluate: typeof millisecondOfEvaluator } = {
49
+ name: 'millisecondOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the millisecond component of a Time or DateTime value (0-999)',
52
+ examples: [
53
+ '@T10:30:45.123.millisecondOf()',
54
+ '@2014-01-05T10:30:45.500.millisecondOf()',
55
+ 'Observation.effectiveDateTime.millisecondOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'millisecondOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: millisecondOfEvaluator
66
+ };
@@ -1,9 +1,9 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { subtractQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { subtractQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
7
 
8
8
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
9
  if (left.length === 0 || right.length === 0) {
@@ -17,6 +17,67 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
17
17
  if (!boxedr) return { value: [], context };
18
18
  const r = unbox(boxedr);
19
19
 
20
+ // Check for temporal arithmetic using boxed type information
21
+ const leftType = boxedl?.typeInfo?.type;
22
+ const rightType = boxedr?.typeInfo?.type;
23
+
24
+ if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
25
+ // Left is temporal, right is quantity
26
+ const temporalType = leftType;
27
+ const quantity = r as QuantityValue;
28
+
29
+ // Import temporal utilities and create TimeQuantity
30
+ const { createTimeQuantity, subtract } = await import('../complex-types/temporal');
31
+
32
+ // Calendar duration units (allowed for temporal arithmetic)
33
+ const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
34
+ 'day', 'days', 'hour', 'hours', 'minute', 'minutes',
35
+ 'second', 'seconds', 'millisecond', 'milliseconds'];
36
+
37
+ // Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
38
+ const variableDurationUnits = ['a', 'mo'];
39
+
40
+ // Fixed duration UCUM units (allowed - they map directly to calendar units)
41
+ const fixedDurationUnitMap: Record<string, string> = {
42
+ 'd': 'day',
43
+ 'wk': 'week',
44
+ 'h': 'hour',
45
+ 'min': 'minute',
46
+ 's': 'second',
47
+ 'ms': 'millisecond'
48
+ };
49
+
50
+ // Check if this is a variable duration unit (not allowed)
51
+ if (variableDurationUnits.includes(quantity.unit)) {
52
+ // Variable duration units like 'a' and 'mo' cannot be subtracted from temporal values
53
+ // because they don't have fixed durations
54
+ const { Errors } = await import('../errors');
55
+ throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
56
+ }
57
+
58
+ // Map fixed duration UCUM units to calendar units
59
+ let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
60
+
61
+ // Check if this is a valid calendar duration unit (after mapping)
62
+ if (!calendarUnits.includes(mappedUnit)) {
63
+ // Non-time units with temporal values return empty per FHIRPath spec
64
+ return { value: [], context };
65
+ }
66
+
67
+ const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
68
+
69
+ // Use the functional subtract operation
70
+ const result = subtract(l as any, timeQuantity);
71
+
72
+ if (temporalType === 'Date') {
73
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
74
+ } else if (temporalType === 'DateTime') {
75
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
76
+ } else if (temporalType === 'Time') {
77
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
78
+ }
79
+ }
80
+
20
81
  // Check if both are quantities
21
82
  if (l && typeof l === 'object' && 'unit' in l &&
22
83
  r && typeof r === 'object' && 'unit' in r) {
@@ -26,7 +87,11 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
26
87
 
27
88
  // Handle numeric subtraction
28
89
  if (typeof l === 'number' && typeof r === 'number') {
29
- return { value: [box(l - r, { type: 'Any', singleton: true })], context };
90
+ const result = l - r;
91
+ const typeInfo = Number.isInteger(result) ?
92
+ { type: 'Integer' as const, singleton: true } :
93
+ { type: 'Decimal' as const, singleton: true };
94
+ return { value: [box(result, typeInfo)], context };
30
95
  }
31
96
 
32
97
  // For other types, return empty
@@ -0,0 +1,66 @@
1
+ // minuteOf() function - Extracts minute component from Time or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRTime, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const minuteOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // minuteOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('minuteOf', 0, args.length);
11
+ }
12
+
13
+ // Empty input returns empty
14
+ if (input.length === 0) {
15
+ return { value: [], context };
16
+ }
17
+
18
+ // Multiple items throws error
19
+ if (input.length > 1) {
20
+ throw Errors.singletonRequired('minuteOf', input.length);
21
+ }
22
+
23
+ const boxedValue = input[0];
24
+ if (!boxedValue) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const value = unbox(boxedValue);
29
+
30
+ // Check if it's a Time or DateTime
31
+ if (isFHIRTime(value) || isFHIRDateTime(value)) {
32
+ // Check if minute component is present
33
+ if (value.minute === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the minute as an Integer (0-59)
38
+ return {
39
+ value: [box(value.minute, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Time or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const minuteOfFunction: FunctionDefinition & { evaluate: typeof minuteOfEvaluator } = {
49
+ name: 'minuteOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the minute component of a Time or DateTime value (0-59)',
52
+ examples: [
53
+ '@T10:30:00.minuteOf()',
54
+ '@2014-01-05T10:30:00.minuteOf()',
55
+ 'Observation.effectiveDateTime.minuteOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'minuteOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: minuteOfEvaluator
66
+ };
@@ -1,7 +1,7 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
5
 
6
6
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
7
  if (left.length === 0 || right.length === 0) {
@@ -0,0 +1,66 @@
1
+ // monthOf() function - Extracts month component from Date or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRDate, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const monthOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // monthOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('monthOf', 0, args.length);
11
+ }
12
+
13
+ // Empty input returns empty
14
+ if (input.length === 0) {
15
+ return { value: [], context };
16
+ }
17
+
18
+ // Multiple items throws error
19
+ if (input.length > 1) {
20
+ throw Errors.singletonRequired('monthOf', input.length);
21
+ }
22
+
23
+ const boxedValue = input[0];
24
+ if (!boxedValue) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const value = unbox(boxedValue);
29
+
30
+ // Check if it's a Date or DateTime
31
+ if (isFHIRDate(value) || isFHIRDateTime(value)) {
32
+ // Check if month component is present
33
+ if (value.month === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the month as an Integer (1-12)
38
+ return {
39
+ value: [box(value.month, { type: 'Integer', singleton: true })],
40
+ context
41
+ };
42
+ }
43
+
44
+ // Not a Date or DateTime, return empty
45
+ return { value: [], context };
46
+ };
47
+
48
+ export const monthOfFunction: FunctionDefinition & { evaluate: typeof monthOfEvaluator } = {
49
+ name: 'monthOf',
50
+ category: ['temporal'],
51
+ description: 'Returns the month component of a Date or DateTime value (1-12)',
52
+ examples: [
53
+ '@2014-01-05.monthOf()',
54
+ '@2014-01-05T10:30:00.monthOf()',
55
+ 'Patient.birthDate.monthOf()'
56
+ ],
57
+ signatures: [
58
+ {
59
+ name: 'monthOf',
60
+ input: { type: 'Any', singleton: true },
61
+ parameters: [],
62
+ result: { type: 'Integer', singleton: true }
63
+ }
64
+ ],
65
+ evaluate: monthOfEvaluator
66
+ };
@@ -1,9 +1,9 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { multiplyQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { multiplyQuantities } from '../complex-types/quantity-value';
5
+ import type { QuantityValue } from '../complex-types/quantity-value';
6
+ import { box, unbox } from '../interpreter/boxing';
7
7
 
8
8
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
9
  if (left.length === 0 || right.length === 0) {
@@ -71,6 +71,30 @@ export const multiplyOperator: OperatorDefinition & { evaluate: OperationEvaluat
71
71
  left: { type: 'Quantity', singleton: true },
72
72
  right: { type: 'Quantity', singleton: true },
73
73
  result: { type: 'Quantity', singleton: true },
74
+ },
75
+ {
76
+ name: 'quantity-integer-multiply',
77
+ left: { type: 'Quantity', singleton: true },
78
+ right: { type: 'Integer', singleton: true },
79
+ result: { type: 'Quantity', singleton: true },
80
+ },
81
+ {
82
+ name: 'integer-quantity-multiply',
83
+ left: { type: 'Integer', singleton: true },
84
+ right: { type: 'Quantity', singleton: true },
85
+ result: { type: 'Quantity', singleton: true },
86
+ },
87
+ {
88
+ name: 'quantity-decimal-multiply',
89
+ left: { type: 'Quantity', singleton: true },
90
+ right: { type: 'Decimal', singleton: true },
91
+ result: { type: 'Quantity', singleton: true },
92
+ },
93
+ {
94
+ name: 'decimal-quantity-multiply',
95
+ left: { type: 'Decimal', singleton: true },
96
+ right: { type: 'Quantity', singleton: true },
97
+ result: { type: 'Quantity', singleton: true },
74
98
  }
75
99
  ],
76
100
  evaluate
@@ -1,33 +1,19 @@
1
- import type { OperatorDefinition } from '../types';
1
+ import type { OperatorDefinition, OperationEvaluator } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
- import type { OperationEvaluator } from '../types';
4
- import { equalQuantities, compareQuantities, type QuantityValue } from '../quantity-value';
5
- import { box, unbox } from '../boxing';
3
+ import { box } from '../interpreter/boxing';
4
+ import { collectionsNotEqual } from './comparison';
6
5
 
7
6
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
8
- if (left.length === 0 || right.length === 0) {
9
- return { value: [], context };
10
- }
11
-
12
- const boxedl = left[0];
13
- if (!boxedl) return { value: [], context };
14
- const l = unbox(boxedl);
15
- const boxedr = right[0];
16
- if (!boxedr) return { value: [], context };
17
- const r = unbox(boxedr);
7
+ // Use the unified comparison system
8
+ const result = collectionsNotEqual(left, right);
18
9
 
19
- // Check if both are quantities
20
- if (l && typeof l === 'object' && 'unit' in l &&
21
- r && typeof r === 'object' && 'unit' in r) {
22
- const comparison = compareQuantities(l as QuantityValue, r as QuantityValue);
23
- // If quantities are incomparable (different dimensions), return empty
24
- if (comparison === null) {
25
- return { value: [], context };
26
- }
27
- return { value: [box(comparison !== 0, { type: 'Boolean', singleton: true })], context };
10
+ // null means incomparable (returns empty)
11
+ if (result === null) {
12
+ return { value: [], context };
28
13
  }
29
14
 
30
- return { value: [box(l !== r, { type: 'Boolean', singleton: true })], context };
15
+ // Return the boolean result
16
+ return { value: [box(result, { type: 'Boolean', singleton: true })], context };
31
17
  };
32
18
 
33
19
  export const notEqualOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -38,11 +24,19 @@ export const notEqualOperator: OperatorDefinition & { evaluate: OperationEvaluat
38
24
  associativity: 'left',
39
25
  description: 'The converse of the equals operator, returning true if equal returns false; false if equal returns true; and empty ({ }) if equal returns empty',
40
26
  examples: ['name != "John"', 'Patient.gender != "male"', '5 != 3'],
41
- signatures: [{
42
- name: 'not-equal',
43
- left: { type: 'Any', singleton: true },
44
- right: { type: 'Any', singleton: true },
45
- result: { type: 'Boolean', singleton: true },
46
- }],
27
+ signatures: [
28
+ {
29
+ name: 'not-equal',
30
+ left: { type: 'Any', singleton: true },
31
+ right: { type: 'Any', singleton: true },
32
+ result: { type: 'Boolean', singleton: true },
33
+ },
34
+ {
35
+ name: 'not-equal',
36
+ left: { type: 'Any', singleton: false },
37
+ right: { type: 'Any', singleton: false },
38
+ result: { type: 'Boolean', singleton: true },
39
+ }
40
+ ],
47
41
  evaluate
48
42
  };
@@ -1,64 +1,23 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { box, unbox } from '../boxing';
4
+ import { box } from '../interpreter/boxing';
5
+ import { collectionsNotEquivalent } from './comparison';
5
6
 
6
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
- // Empty collections are equivalent, so not-equivalent returns false
8
- if (left.length === 0 && right.length === 0) {
9
- return { value: [box(false, { type: 'Boolean', singleton: true })], context };
10
- }
11
-
12
- // Different sizes are not equivalent, so not-equivalent returns true
13
- if (left.length !== right.length) {
14
- return { value: [box(true, { type: 'Boolean', singleton: true })], context };
15
- }
16
-
17
- // For single items, check type-specific non-equivalence
18
- if (left.length === 1 && right.length === 1) {
19
- const boxedl = left[0];
20
- if (!boxedl) return { value: [], context };
21
- const l = unbox(boxedl);
22
- const boxedr = right[0];
23
- if (!boxedr) return { value: [], context };
24
- const r = unbox(boxedr);
25
-
26
- // String non-equivalence: case-insensitive with normalized whitespace
27
- if (typeof l === 'string' && typeof r === 'string') {
28
- const normalizeString = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
29
- return { value: [box(normalizeString(l) !== normalizeString(r), { type: 'Boolean', singleton: true })], context };
30
- }
31
-
32
- // Number non-equivalence (for Integer and Decimal)
33
- if (typeof l === 'number' && typeof r === 'number') {
34
- // For decimals with different precision, round to least precise
35
- // This is a simplified implementation
36
- return { value: [box(Math.abs(l - r) >= Number.EPSILON, { type: 'Boolean', singleton: true })], context };
37
- }
38
-
39
- // Boolean non-equivalence
40
- if (typeof l === 'boolean' && typeof r === 'boolean') {
41
- return { value: [box(l !== r, { type: 'Boolean', singleton: true })], context };
42
- }
43
-
44
- // For complex types and other cases, use inequality for now
45
- // TODO: Implement full non-equivalence logic for Date/DateTime/Time and complex types
46
- return { value: [box(l !== r, { type: 'Boolean', singleton: true })], context };
47
- }
8
+ // Use the new collectionsNotEquivalent function from comparison.ts
9
+ const result = collectionsNotEquivalent(left, right);
48
10
 
49
- // For multiple items, comparison is order-independent
50
- // Create sorted copies for comparison
51
- // TODO: Implement proper order-independent comparison with recursive equivalence
52
- const leftSorted = [...left].sort();
53
- const rightSorted = [...right].sort();
54
-
55
- for (let i = 0; i < leftSorted.length; i++) {
56
- if (leftSorted[i] !== rightSorted[i]) {
57
- return { value: [box(true, { type: 'Boolean', singleton: true })], context };
58
- }
11
+ // null result means incomparable - return empty collection
12
+ if (result === null) {
13
+ return { value: [], context };
59
14
  }
60
15
 
61
- return { value: [box(false, { type: 'Boolean', singleton: true })], context };
16
+ // Return boolean result
17
+ return {
18
+ value: [box(result, { type: 'Boolean', singleton: true })],
19
+ context
20
+ };
62
21
  };
63
22
 
64
23
  export const notEquivalentOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -67,6 +26,7 @@ export const notEquivalentOperator: OperatorDefinition & { evaluate: OperationEv
67
26
  category: ['equality'],
68
27
  precedence: PRECEDENCE.EQUALITY,
69
28
  associativity: 'left',
29
+ doesNotPropagateEmpty: true, // Empty collections are valid operands for not-equivalence
70
30
  description: 'The converse of the equivalent operator, returning true if equivalent returns false and false if equivalent returns true. In other words, A !~ B is short-hand for (A ~ B).not()',
71
31
  examples: [
72
32
  "'abc' !~ 'ABC'",
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import type { FunctionEvaluator } from '../types';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  // Three-valued logic implementation
@@ -1,9 +1,8 @@
1
1
  import type { FunctionDefinition, RuntimeContext, ASTNode, TypeInfo, NodeEvaluator, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
3
- import type { FHIRPathValue } from '../boxing';
4
- import { unbox } from '../boxing';
3
+ import type { FHIRPathValue } from '../interpreter/boxing';
4
+ import { unbox } from '../interpreter/boxing';
5
5
  import { isIdentifierNode, isFunctionNode } from '../types';
6
- import { NodeType } from '../types';
7
6
 
8
7
  export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
9
8
  name: 'ofType',
@@ -21,7 +20,8 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
21
20
  {
22
21
  name: 'type',
23
22
  type: { type: 'Any', singleton: true },
24
- expression: true
23
+ expression: true,
24
+ typeReference: true // This parameter expects a type name
25
25
  }
26
26
  ],
27
27
  result: 'inputType'
@@ -37,10 +37,6 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
37
37
  let targetTypeName: string;
38
38
  if (isIdentifierNode(typeArg)) {
39
39
  targetTypeName = typeArg.name;
40
- } else if (typeArg.type === NodeType.TypeOrIdentifier) {
41
- targetTypeName = (typeArg as any).name;
42
- } else if (typeArg.type === NodeType.TypeReference) {
43
- targetTypeName = (typeArg as any).name;
44
40
  } else if (isFunctionNode(typeArg) && isIdentifierNode(typeArg.name)) {
45
41
  // Handle cases like ofType(Patient())
46
42
  targetTypeName = typeArg.name.name;
@@ -91,9 +87,9 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
91
87
  return matchingType !== undefined;
92
88
  }
93
89
 
94
- // Check if the box has type information
95
- if (boxedItem.typeInfo) {
96
- // If we have type info, use it for accurate filtering
90
+ // Check if the box has specific type information (not just "Any")
91
+ if (boxedItem.typeInfo && boxedItem.typeInfo.type !== 'Any') {
92
+ // If we have specific type info, use it for accurate filtering
97
93
  return boxedItem.typeInfo.type === targetTypeName;
98
94
  }
99
95
 
@@ -136,4 +132,4 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
136
132
 
137
133
  return { value: actualFiltered, context };
138
134
  }
139
- };
135
+ };