@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
@@ -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
@@ -9,6 +9,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
9
9
  return { value: [], context };
10
10
  }
11
11
 
12
+ // Multiple values should throw an error per XML tests
13
+ if (input.length > 1) {
14
+ throw new Error('not() can only be applied to a single item');
15
+ }
16
+
12
17
  const boxedValue = input[0];
13
18
  if (!boxedValue) {
14
19
  return { value: [], context };
@@ -24,8 +29,10 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
24
29
  return { value: [box(true, { type: 'Boolean', singleton: true })], context };
25
30
  }
26
31
 
27
- // Non-boolean values return empty
28
- return { value: [], context };
32
+ // Non-boolean values: any non-empty collection is considered truthy
33
+ // So not() returns false for any non-boolean value
34
+ // This follows the XML test suite behavior
35
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
29
36
  };
30
37
 
31
38
  export const notFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
@@ -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;
@@ -48,6 +44,41 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
48
44
  throw Errors.invalidOperation(`ofType() requires a type name as argument, got ${typeArg.type}`);
49
45
  }
50
46
 
47
+ // Validate the type name using ModelProvider if available
48
+ if (context.modelProvider) {
49
+ // Try to get the type from the model provider
50
+ const typeInfo = await context.modelProvider.getType(targetTypeName);
51
+ if (!typeInfo) {
52
+ // Not a valid FHIR type, check if it's a System type
53
+ const systemTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity'];
54
+ const fhirPrimitiveTypes = ['boolean', 'integer', 'string', 'decimal', 'uri', 'url', 'canonical',
55
+ 'base64Binary', 'instant', 'date', 'dateTime', 'time', 'code', 'oid',
56
+ 'id', 'markdown', 'unsignedInt', 'positiveInt', 'uuid', 'xhtml'];
57
+
58
+ if (!systemTypes.includes(targetTypeName) && !fhirPrimitiveTypes.includes(targetTypeName)) {
59
+ throw Errors.invalidOperation(`Unknown type: ${targetTypeName}`);
60
+ }
61
+ }
62
+ } else {
63
+ // Without ModelProvider, only allow basic System types and reject obvious invalid names
64
+ const systemTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
65
+ const fhirPrimitiveTypes = ['boolean', 'integer', 'string', 'decimal', 'uri', 'url', 'canonical',
66
+ 'base64Binary', 'instant', 'date', 'dateTime', 'time', 'code', 'oid',
67
+ 'id', 'markdown', 'unsignedInt', 'positiveInt', 'uuid'];
68
+
69
+ // If it's not a known primitive type and looks invalid, reject it
70
+ if (!systemTypes.includes(targetTypeName) && !fhirPrimitiveTypes.includes(targetTypeName)) {
71
+ // Check if it looks like a valid type name
72
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(targetTypeName) && !/^[a-z][a-z0-9]*$/i.test(targetTypeName)) {
73
+ throw Errors.invalidOperation(`Invalid type name: ${targetTypeName}`);
74
+ }
75
+ // If it contains numbers but isn't a known type, it's likely invalid
76
+ if (/\d/.test(targetTypeName) && targetTypeName !== 'base64Binary') {
77
+ throw Errors.invalidOperation(`Unknown type: ${targetTypeName}`);
78
+ }
79
+ }
80
+ }
81
+
51
82
  // If we have typeInfo from the analyzer (with ModelProvider), use it
52
83
  // NOTE: This optimization is currently disabled because currentNode refers to the ofType
53
84
  // function node, not the input navigation node. The correct type checking happens below
@@ -91,9 +122,9 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
91
122
  return matchingType !== undefined;
92
123
  }
93
124
 
94
- // Check if the box has type information
95
- if (boxedItem.typeInfo) {
96
- // If we have type info, use it for accurate filtering
125
+ // Check if the box has specific type information (not just "Any")
126
+ if (boxedItem.typeInfo && boxedItem.typeInfo.type !== 'Any') {
127
+ // If we have specific type info, use it for accurate filtering
97
128
  return boxedItem.typeInfo.type === targetTypeName;
98
129
  }
99
130
 
@@ -136,4 +167,4 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
136
167
 
137
168
  return { value: actualFiltered, context };
138
169
  }
139
- };
170
+ };
@@ -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
  // Three-valued logic implementation
@@ -30,6 +30,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
30
30
  export const orOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
31
31
  symbol: 'or',
32
32
  name: 'or',
33
+ doesNotPropagateEmpty: true, // Three-valued logic: true or empty = true
33
34
  category: ['logical'],
34
35
  precedence: PRECEDENCE.OR,
35
36
  associativity: 'left',
@@ -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 { addQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { addQuantities } 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) {
@@ -20,6 +20,69 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
20
20
  const l = unbox(boxedL);
21
21
  const r = unbox(boxedR);
22
22
 
23
+ // Check for temporal arithmetic using boxed type information
24
+ const leftType = boxedL?.typeInfo?.type;
25
+ const rightType = boxedR?.typeInfo?.type;
26
+
27
+ if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
28
+ // Left is temporal, right is quantity
29
+ const temporalType = leftType;
30
+ const temporal = l;
31
+ const quantity = r as QuantityValue;
32
+
33
+ // Import temporal utilities and create TimeQuantity
34
+ const { createTimeQuantity, add } = await import('../complex-types/temporal');
35
+
36
+ // Calendar duration units (allowed for temporal arithmetic)
37
+ const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
38
+ 'day', 'days', 'hour', 'hours', 'minute', 'minutes',
39
+ 'second', 'seconds', 'millisecond', 'milliseconds'];
40
+
41
+ // Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
42
+ const variableDurationUnits = ['a', 'mo'];
43
+
44
+ // Fixed duration UCUM units (allowed - they map directly to calendar units)
45
+ const fixedDurationUnitMap: Record<string, string> = {
46
+ 'd': 'day',
47
+ 'wk': 'week',
48
+ 'h': 'hour',
49
+ 'min': 'minute',
50
+ 's': 'second',
51
+ 'ms': 'millisecond'
52
+ };
53
+
54
+ // Check if this is a variable duration unit (not allowed)
55
+ if (variableDurationUnits.includes(quantity.unit)) {
56
+ // Variable duration units like 'a' and 'mo' cannot be added to temporal values
57
+ // because they don't have fixed durations
58
+ const { Errors } = await import('../errors');
59
+ throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
60
+ }
61
+
62
+ // Map fixed duration UCUM units to calendar units
63
+ let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
64
+
65
+ // Check if this is a valid calendar duration unit (after mapping)
66
+ if (!calendarUnits.includes(mappedUnit)) {
67
+ // Non-time units with temporal values return empty per FHIRPath spec
68
+ return { value: [], context };
69
+ }
70
+
71
+ const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
72
+
73
+ // Use the functional add operation
74
+ const result = add(temporal as any, timeQuantity);
75
+
76
+ if (temporalType === 'Date') {
77
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
78
+ } else if (temporalType === 'DateTime') {
79
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
80
+ } else if (temporalType === 'Time') {
81
+ // Let the error propagate - adding calendar units to Time should throw
82
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
83
+ }
84
+ }
85
+
23
86
  // Check if both are quantities
24
87
  if (l && typeof l === 'object' && 'unit' in l &&
25
88
  r && typeof r === 'object' && 'unit' in r) {
@@ -27,8 +90,9 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
27
90
  return { value: result ? [box(result, { type: 'Quantity', singleton: true })] : [], context };
28
91
  }
29
92
 
30
- if (typeof l === 'string' || typeof r === 'string') {
31
- return { value: [box(String(l) + String(r), { type: 'String', singleton: true })], context };
93
+ // String concatenation only works for string + string
94
+ if (typeof l === 'string' && typeof r === 'string') {
95
+ return { value: [box(l + r, { type: 'String', singleton: true })], context };
32
96
  }
33
97
 
34
98
  if (typeof l === 'number' && typeof r === 'number') {
@@ -39,8 +103,8 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
39
103
  return { value: [box(result, typeInfo)], context };
40
104
  }
41
105
 
42
- // For other types, convert to string
43
- return { value: [box(String(l) + String(r), { type: 'String', singleton: true })], context };
106
+ // For incompatible types, return empty per FHIRPath spec
107
+ return { value: [], context };
44
108
  };
45
109
 
46
110
  export const plusOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // power() takes exactly one argument (exponent)
@@ -75,14 +75,39 @@ export const powerFunction: FunctionDefinition & { evaluate: FunctionEvaluator }
75
75
  '2.5.power(2)',
76
76
  '(-1).power(0.5)'
77
77
  ],
78
- signatures: [{
79
-
80
- name: 'power',
81
- input: { type: 'Decimal', singleton: true },
82
- parameters: [
83
- { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
84
- ],
85
- result: { type: 'Decimal', singleton: true }
86
- }],
78
+ signatures: [
79
+ {
80
+ name: 'power-integer',
81
+ input: { type: 'Integer', singleton: true },
82
+ parameters: [
83
+ { name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
84
+ ],
85
+ result: { type: 'Integer', singleton: true }
86
+ },
87
+ {
88
+ name: 'power-decimal',
89
+ input: { type: 'Decimal', singleton: true },
90
+ parameters: [
91
+ { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
92
+ ],
93
+ result: { type: 'Decimal', singleton: true }
94
+ },
95
+ {
96
+ name: 'power-integer-decimal',
97
+ input: { type: 'Integer', singleton: true },
98
+ parameters: [
99
+ { name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
100
+ ],
101
+ result: { type: 'Decimal', singleton: true }
102
+ },
103
+ {
104
+ name: 'power-decimal-integer',
105
+ input: { type: 'Decimal', singleton: true },
106
+ parameters: [
107
+ { name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
108
+ ],
109
+ result: { type: 'Decimal', singleton: true }
110
+ }
111
+ ],
87
112
  evaluate
88
113
  };
@@ -0,0 +1,146 @@
1
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { getDecimalPlaces } from '../utils/decimal';
5
+ import { isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
6
+
7
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
8
+ // Check single item in input
9
+ if (input.length === 0) {
10
+ return { value: [], context };
11
+ }
12
+
13
+ if (input.length > 1) {
14
+ throw Errors.singletonRequired('precision', input.length);
15
+ }
16
+
17
+ const boxedInputValue = input[0];
18
+ if (!boxedInputValue) {
19
+ return { value: [], context };
20
+ }
21
+
22
+ const inputValue = unbox(boxedInputValue);
23
+
24
+ // precision() takes no arguments
25
+ if (args.length !== 0) {
26
+ throw Errors.wrongArgumentCount('precision', 0, args.length);
27
+ }
28
+
29
+ // Handle temporal types - return precision according to FHIRPath spec
30
+ if (isFHIRDate(inputValue)) {
31
+ const date = inputValue as any;
32
+ // Date precision: year=4, year-month=6, year-month-day=8
33
+ if (date.day !== undefined) {
34
+ return { value: [box(8, { type: 'Integer', singleton: true })], context };
35
+ } else if (date.month !== undefined) {
36
+ return { value: [box(6, { type: 'Integer', singleton: true })], context };
37
+ } else {
38
+ return { value: [box(4, { type: 'Integer', singleton: true })], context };
39
+ }
40
+ }
41
+
42
+ if (isFHIRDateTime(inputValue)) {
43
+ const dateTime = inputValue as any;
44
+ // DateTime precision: year=4, month=6, day=8, hour=10, minute=12, second=14, millisecond=17
45
+ if (dateTime.millisecond !== undefined) {
46
+ return { value: [box(17, { type: 'Integer', singleton: true })], context };
47
+ } else if (dateTime.second !== undefined) {
48
+ return { value: [box(14, { type: 'Integer', singleton: true })], context };
49
+ } else if (dateTime.minute !== undefined) {
50
+ return { value: [box(12, { type: 'Integer', singleton: true })], context };
51
+ } else if (dateTime.hour !== undefined) {
52
+ return { value: [box(10, { type: 'Integer', singleton: true })], context };
53
+ } else if (dateTime.day !== undefined) {
54
+ return { value: [box(8, { type: 'Integer', singleton: true })], context };
55
+ } else if (dateTime.month !== undefined) {
56
+ return { value: [box(6, { type: 'Integer', singleton: true })], context };
57
+ } else {
58
+ return { value: [box(4, { type: 'Integer', singleton: true })], context };
59
+ }
60
+ }
61
+
62
+ if (isFHIRTime(inputValue)) {
63
+ const time = inputValue as any;
64
+ // Time precision: hour=2, minute=4, second=6, millisecond=9
65
+ // But spec shows hour=4, minute=4, second=6, millisecond=9
66
+ // Looking at examples: @T10:30 has "10:30" which is 4 characters for HH:mm
67
+ // @T10:30:00.000 has milliseconds which would be 9 for HH:mm:ss.fff
68
+ if (time.millisecond !== undefined) {
69
+ return { value: [box(9, { type: 'Integer', singleton: true })], context };
70
+ } else if (time.second !== undefined) {
71
+ return { value: [box(6, { type: 'Integer', singleton: true })], context };
72
+ } else if (time.minute !== undefined) {
73
+ return { value: [box(4, { type: 'Integer', singleton: true })], context };
74
+ } else {
75
+ // Hour only would be 2 (HH)
76
+ return { value: [box(2, { type: 'Integer', singleton: true })], context };
77
+ }
78
+ }
79
+
80
+ // Handle decimal/numeric types
81
+ if (typeof inputValue === 'number') {
82
+ // For integers, precision is always 0
83
+ if (Number.isInteger(inputValue)) {
84
+ return { value: [box(0, { type: 'Integer', singleton: true })], context };
85
+ }
86
+
87
+ // For decimals, count significant digits after the decimal point
88
+ const decimalPlaces = getDecimalPlaces(inputValue);
89
+ return { value: [box(decimalPlaces, { type: 'Integer', singleton: true })], context };
90
+ }
91
+
92
+ // For any other type, return empty
93
+ return { value: [], context };
94
+ };
95
+
96
+ export const precisionFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
97
+ name: 'precision',
98
+ category: ['utility'],
99
+ description: 'Returns the number of digits of precision for decimal values, or the precision indicator for temporal values (year=4, month=6, day=8, etc.). Returns empty for other types.',
100
+ examples: [
101
+ "1.58700.precision()",
102
+ "@2014.precision()",
103
+ "@2014-01-05T10:30:00.000.precision()",
104
+ "@T10:30.precision()",
105
+ "{}.precision()"
106
+ ],
107
+ signatures: [
108
+ {
109
+ name: 'precision',
110
+ input: { type: 'Decimal', singleton: true },
111
+ parameters: [],
112
+ result: { type: 'Integer', singleton: true }
113
+ },
114
+ {
115
+ name: 'precision',
116
+ input: { type: 'Integer', singleton: true },
117
+ parameters: [],
118
+ result: { type: 'Integer', singleton: true }
119
+ },
120
+ {
121
+ name: 'precision',
122
+ input: { type: 'Date', singleton: true },
123
+ parameters: [],
124
+ result: { type: 'Integer', singleton: true }
125
+ },
126
+ {
127
+ name: 'precision',
128
+ input: { type: 'DateTime', singleton: true },
129
+ parameters: [],
130
+ result: { type: 'Integer', singleton: true }
131
+ },
132
+ {
133
+ name: 'precision',
134
+ input: { type: 'Time', singleton: true },
135
+ parameters: [],
136
+ result: { type: 'Integer', singleton: true }
137
+ },
138
+ {
139
+ name: 'precision',
140
+ input: { type: 'Any', singleton: true },
141
+ parameters: [],
142
+ result: { type: 'Integer', singleton: false }
143
+ }
144
+ ],
145
+ evaluate
146
+ };