@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,86 @@
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('matches', 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('matches');
23
+ }
24
+
25
+ // Check arguments
26
+ if (args.length !== 1) {
27
+ throw Errors.wrongArgumentCount('matches', 1, args.length);
28
+ }
29
+
30
+ // Evaluate regex argument
31
+ const regexArg = args[0];
32
+ if (!regexArg) {
33
+ throw Errors.argumentRequired('matches', '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('matches 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('matches', 'regex argument');
54
+ }
55
+
56
+ try {
57
+ // Create regex with unicode support and single line mode (dotAll)
58
+ // Per spec: case-sensitive, single line mode, allow Unicode
59
+ const regex = new RegExp(regexPattern, 'us');
60
+ const result = regex.test(inputValue);
61
+
62
+ return { value: [box(result, { type: 'Boolean', singleton: true })], context };
63
+ } catch (error) {
64
+ throw new Error(`Invalid regular expression in matches(): ${(error as Error).message}`);
65
+ }
66
+ };
67
+
68
+ export const matchesFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
69
+ name: 'matches',
70
+ category: ['string'],
71
+ description: 'Returns true when the input string matches the given regular expression',
72
+ examples: [
73
+ "'test string'.matches('t.+')",
74
+ "'test string'.matches('asd.+')",
75
+ "'first line\\nsecond line'.matches('line.second')"
76
+ ],
77
+ signatures: [{
78
+ name: 'matches',
79
+ input: { type: 'String', singleton: true },
80
+ parameters: [
81
+ { name: 'regex', type: { type: 'String', singleton: true } }
82
+ ],
83
+ result: { type: 'Boolean', singleton: true }
84
+ }],
85
+ evaluate
86
+ };
@@ -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,10 @@
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
+ import { normalizeDecimalResult } from '../utils/decimal';
7
8
 
8
9
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
10
  if (left.length === 0 || right.length === 0) {
@@ -17,6 +18,67 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
17
18
  if (!boxedr) return { value: [], context };
18
19
  const r = unbox(boxedr);
19
20
 
21
+ // Check for temporal arithmetic using boxed type information
22
+ const leftType = boxedl?.typeInfo?.type;
23
+ const rightType = boxedr?.typeInfo?.type;
24
+
25
+ if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
26
+ // Left is temporal, right is quantity
27
+ const temporalType = leftType;
28
+ const quantity = r as QuantityValue;
29
+
30
+ // Import temporal utilities and create TimeQuantity
31
+ const { createTimeQuantity, subtract } = await import('../complex-types/temporal');
32
+
33
+ // Calendar duration units (allowed for temporal arithmetic)
34
+ const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
35
+ 'day', 'days', 'hour', 'hours', 'minute', 'minutes',
36
+ 'second', 'seconds', 'millisecond', 'milliseconds'];
37
+
38
+ // Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
39
+ const variableDurationUnits = ['a', 'mo'];
40
+
41
+ // Fixed duration UCUM units (allowed - they map directly to calendar units)
42
+ const fixedDurationUnitMap: Record<string, string> = {
43
+ 'd': 'day',
44
+ 'wk': 'week',
45
+ 'h': 'hour',
46
+ 'min': 'minute',
47
+ 's': 'second',
48
+ 'ms': 'millisecond'
49
+ };
50
+
51
+ // Check if this is a variable duration unit (not allowed)
52
+ if (variableDurationUnits.includes(quantity.unit)) {
53
+ // Variable duration units like 'a' and 'mo' cannot be subtracted from temporal values
54
+ // because they don't have fixed durations
55
+ const { Errors } = await import('../errors');
56
+ throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
57
+ }
58
+
59
+ // Map fixed duration UCUM units to calendar units
60
+ let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
61
+
62
+ // Check if this is a valid calendar duration unit (after mapping)
63
+ if (!calendarUnits.includes(mappedUnit)) {
64
+ // Non-time units with temporal values return empty per FHIRPath spec
65
+ return { value: [], context };
66
+ }
67
+
68
+ const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
69
+
70
+ // Use the functional subtract operation
71
+ const result = subtract(l as any, timeQuantity);
72
+
73
+ if (temporalType === 'Date') {
74
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
75
+ } else if (temporalType === 'DateTime') {
76
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
77
+ } else if (temporalType === 'Time') {
78
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
79
+ }
80
+ }
81
+
20
82
  // Check if both are quantities
21
83
  if (l && typeof l === 'object' && 'unit' in l &&
22
84
  r && typeof r === 'object' && 'unit' in r) {
@@ -26,7 +88,17 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
26
88
 
27
89
  // Handle numeric subtraction
28
90
  if (typeof l === 'number' && typeof r === 'number') {
29
- return { value: [box(l - r, { type: 'Any', singleton: true })], context };
91
+ let result = l - r;
92
+
93
+ // Normalize decimal result to handle floating point precision issues
94
+ if (!Number.isInteger(l) || !Number.isInteger(r)) {
95
+ result = normalizeDecimalResult(result, l, r);
96
+ }
97
+
98
+ const typeInfo = Number.isInteger(result) ?
99
+ { type: 'Integer' as const, singleton: true } :
100
+ { type: 'Decimal' as const, singleton: true };
101
+ return { value: [box(result, typeInfo)], context };
30
102
  }
31
103
 
32
104
  // 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,8 @@
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
+ import { normalizeModuloResult } from '../utils/decimal';
5
6
 
6
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
8
  if (left.length === 0 || right.length === 0) {
@@ -21,7 +22,12 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
21
22
  return { value: [], context };
22
23
  }
23
24
 
24
- const result = (leftValue as any) % (rightValue as any);
25
+ let result = (leftValue as any) % (rightValue as any);
26
+
27
+ // Normalize decimal result to handle floating point precision issues
28
+ if (!Number.isInteger(leftValue) || !Number.isInteger(rightValue)) {
29
+ result = normalizeModuloResult(result, leftValue as number, rightValue as number);
30
+ }
25
31
 
26
32
  // Determine result type based on input types
27
33
  const resultType = Number.isInteger(leftValue) && Number.isInteger(rightValue) ? 'Integer' : 'Decimal';
@@ -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
  };