@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,7 +1,8 @@
1
1
  import type { OperatorDefinition, TypeName } 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 { isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
5
6
 
6
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
7
8
  // Right operand should be a type identifier
@@ -13,9 +14,132 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
13
14
  const boxedItem = left[0];
14
15
  const item = unbox(boxedItem);
15
16
  const typeName = right[0] as string; // Should be a type name like 'String', 'Integer', etc.
16
-
17
- // If we have a ModelProvider in context, use it for accurate type checking
17
+ // If we have a ModelProvider and typeInfo, use it for accurate type checking (handles subtypes)
18
18
  if (context.modelProvider && boxedItem?.typeInfo) {
19
+ // Check for namespace-qualified types (e.g., System.Boolean, FHIR.boolean)
20
+ let checkNamespace: string | undefined;
21
+ let checkType: string;
22
+
23
+ if (typeName.includes('.')) {
24
+ const parts = typeName.split('.');
25
+ checkNamespace = parts[0];
26
+ checkType = parts.slice(1).join('.');
27
+ } else {
28
+ checkType = typeName;
29
+ }
30
+
31
+ // If checking for System type, ensure it's NOT a FHIR type
32
+ if (checkNamespace === 'System') {
33
+ // System types should not match FHIR types
34
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
35
+ return {
36
+ value: [box(false, { type: 'Boolean', singleton: true })],
37
+ context
38
+ };
39
+ }
40
+ // Check if the type matches (without namespace)
41
+ const normalizedType = checkType;
42
+ if (boxedItem.typeInfo.type === normalizedType) {
43
+ return {
44
+ value: [box(true, { type: 'Boolean', singleton: true })],
45
+ context
46
+ };
47
+ }
48
+ }
49
+
50
+ // If checking for FHIR type
51
+ if (checkNamespace === 'FHIR') {
52
+ // Must be a FHIR namespaced type
53
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
54
+ // Check the name field for FHIR types (e.g., 'boolean', 'integer')
55
+ if (boxedItem.typeInfo.name === checkType) {
56
+ return {
57
+ value: [box(true, { type: 'Boolean', singleton: true })],
58
+ context
59
+ };
60
+ }
61
+ // Also check if asking for a resource type like FHIR.Patient
62
+ if (boxedItem.typeInfo.type === checkType || boxedItem.typeInfo.name === checkType) {
63
+ return {
64
+ value: [box(true, { type: 'Boolean', singleton: true })],
65
+ context
66
+ };
67
+ }
68
+ }
69
+ return {
70
+ value: [box(false, { type: 'Boolean', singleton: true })],
71
+ context
72
+ };
73
+ }
74
+
75
+ // When no namespace is specified (e.g., just "Boolean", "String", "string", "code")
76
+ if (!checkNamespace) {
77
+ // Check for System primitive types (capitalized)
78
+ const systemPrimitiveTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
79
+ if (systemPrimitiveTypes.includes(checkType)) {
80
+ // For System primitive types, only match if NOT a FHIR type
81
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
82
+ return {
83
+ value: [box(false, { type: 'Boolean', singleton: true })],
84
+ context
85
+ };
86
+ }
87
+ // Check if the type matches
88
+ if (boxedItem.typeInfo.type === checkType) {
89
+ return {
90
+ value: [box(true, { type: 'Boolean', singleton: true })],
91
+ context
92
+ };
93
+ }
94
+ }
95
+
96
+ // Check for FHIR primitive types (lowercase) like string, code, id, etc.
97
+ const fhirPrimitiveTypes = ['string', 'code', 'id', 'uri', 'url', 'canonical', 'uuid', 'oid',
98
+ 'boolean', 'integer', 'decimal', 'base64Binary', 'instant',
99
+ 'date', 'dateTime', 'time', 'unsignedInt', 'positiveInt', 'markdown'];
100
+ if (fhirPrimitiveTypes.includes(checkType)) {
101
+ // For FHIR primitive types, check both the name and underlying type
102
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
103
+ // Check if the name matches
104
+ if (boxedItem.typeInfo.name === checkType) {
105
+ return {
106
+ value: [box(true, { type: 'Boolean', singleton: true })],
107
+ context
108
+ };
109
+ }
110
+ // For 'string', also match code, id, etc. (they are all string-based)
111
+ if (checkType === 'string') {
112
+ const stringBasedTypes = ['code', 'id', 'uri', 'url', 'canonical', 'uuid', 'oid', 'markdown'];
113
+ if (boxedItem.typeInfo.name && stringBasedTypes.includes(boxedItem.typeInfo.name)) {
114
+ return {
115
+ value: [box(true, { type: 'Boolean', singleton: true })],
116
+ context
117
+ };
118
+ }
119
+ // Also check if underlying type is String
120
+ if (boxedItem.typeInfo.type === 'String') {
121
+ return {
122
+ value: [box(true, { type: 'Boolean', singleton: true })],
123
+ context
124
+ };
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ value: [box(false, { type: 'Boolean', singleton: true })],
130
+ context
131
+ };
132
+ }
133
+
134
+ // For non-primitive types (like Patient, Observation), use model provider
135
+ const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as TypeName);
136
+ return {
137
+ value: [box(matchingType !== undefined, { type: 'Boolean', singleton: true })],
138
+ context
139
+ };
140
+ }
141
+
142
+ // Default case - use model provider's ofType for other checks
19
143
  const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as TypeName);
20
144
  return {
21
145
  value: [box(matchingType !== undefined, { type: 'Boolean', singleton: true })],
@@ -23,11 +147,24 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
23
147
  };
24
148
  }
25
149
 
26
- // Check if the box has type information
150
+ // Check if the box has type information (without ModelProvider, check with normalization)
27
151
  if (boxedItem?.typeInfo) {
28
- // For now, just check exact type match (no subtype support without ModelProvider)
152
+ // Normalize both types for comparison
153
+ let boxedType = boxedItem.typeInfo.type;
154
+ let compareType = typeName;
155
+
156
+ // Normalize System.X to X for primitive types
157
+ if (compareType.startsWith('System.')) {
158
+ compareType = compareType.substring(7);
159
+ }
160
+
161
+ // Also check if boxedType needs normalization (unlikely but consistent)
162
+ if (boxedType.startsWith('System.')) {
163
+ boxedType = boxedType.substring(7);
164
+ }
165
+
29
166
  return {
30
- value: [box(boxedItem.typeInfo.type === typeName, { type: 'Boolean', singleton: true })],
167
+ value: [box(boxedType === compareType, { type: 'Boolean', singleton: true })],
31
168
  context
32
169
  };
33
170
  }
@@ -35,9 +172,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
35
172
  // For FHIR resources without typeInfo, try to get it from modelProvider
36
173
  if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
37
174
  // Use cached type if available
38
- const typeInfo = 'getTypeFromCache' in context.modelProvider
39
- ? (context.modelProvider as any).getTypeFromCache(item.resourceType)
40
- : undefined;
175
+ const typeInfo = await context.modelProvider.getType(item.resourceType);
41
176
  if (typeInfo) {
42
177
  const matchingType = context.modelProvider.ofType(typeInfo, typeName as TypeName);
43
178
  return {
@@ -52,21 +187,49 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
52
187
  };
53
188
  }
54
189
 
55
- // Check primitive types
56
- switch (typeName) {
190
+ // Normalize type names - strip namespace for System types
191
+ // System.Boolean -> Boolean, FHIR.boolean -> boolean, etc.
192
+ let normalizedTypeName = typeName;
193
+ if (typeName.startsWith('System.')) {
194
+ normalizedTypeName = typeName.substring(7); // Remove "System."
195
+ } else if (typeName.startsWith('FHIR.')) {
196
+ // For FHIR types, keep the namespace for model provider lookup
197
+ // but also check without namespace for primitive types
198
+ normalizedTypeName = typeName;
199
+ }
200
+
201
+ // Check primitive types (with normalized names)
202
+ switch (normalizedTypeName) {
57
203
  case 'String':
204
+ case 'System.String':
58
205
  return { value: [box(typeof item === 'string', { type: 'Boolean', singleton: true })], context };
59
206
  case 'Boolean':
207
+ case 'System.Boolean':
60
208
  return { value: [box(typeof item === 'boolean', { type: 'Boolean', singleton: true })], context };
61
209
  case 'Integer':
210
+ case 'System.Integer':
62
211
  return { value: [box(typeof item === 'number' && Number.isInteger(item), { type: 'Boolean', singleton: true })], context };
63
212
  case 'Decimal':
213
+ case 'System.Decimal':
64
214
  return { value: [box(typeof item === 'number', { type: 'Boolean', singleton: true })], context };
65
215
  case 'Date':
216
+ // Check if it's a FHIRDate instance or has Date type
217
+ if (item && typeof item === 'object') {
218
+ return { value: [box(isFHIRDate(item) || (item as any).kind === 'FHIRDate', { type: 'Boolean', singleton: true })], context };
219
+ }
220
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
66
221
  case 'DateTime':
222
+ // Check if it's a FHIRDateTime instance or has DateTime type
223
+ if (item && typeof item === 'object') {
224
+ return { value: [box(isFHIRDateTime(item) || (item as any).kind === 'FHIRDateTime', { type: 'Boolean', singleton: true })], context };
225
+ }
226
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
67
227
  case 'Time':
68
- // Simple check for date-like strings
69
- return { value: [box(typeof item === 'string' && !isNaN(Date.parse(item)), { type: 'Boolean', singleton: true })], context };
228
+ // Check if it's a FHIRTime instance or has Time type
229
+ if (item && typeof item === 'object') {
230
+ return { value: [box(isFHIRTime(item) || (item as any).kind === 'FHIRTime', { type: 'Boolean', singleton: true })], context };
231
+ }
232
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
70
233
  default:
71
234
  // For complex types, check resourceType
72
235
  if (item && typeof item === 'object' && 'resourceType' in item) {
@@ -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
  // isDistinct takes no arguments
@@ -35,6 +35,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
35
35
 
36
36
  export const isDistinctFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
37
37
  name: 'isDistinct',
38
+ doesNotPropagateEmpty: true, // Returns true for empty collections
38
39
  category: ['existence'],
39
40
  description: 'Returns true if all the items in the input collection are distinct. To determine whether two items are distinct, the equals (=) operator is used. If the input collection is empty, the result is true.',
40
41
  examples: [
@@ -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
  // If input is empty, return empty
@@ -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
  if (input.length > 0) {
@@ -0,0 +1,85 @@
1
+ import type { FunctionDefinition, FunctionEvaluator, LiteralNode } 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('lastIndexOf', 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('lastIndexOf');
23
+ }
24
+
25
+ // Check arguments
26
+ if (args.length !== 1) {
27
+ throw Errors.wrongArgumentCount('lastIndexOf', 1, args.length);
28
+ }
29
+
30
+ // Evaluate substring argument
31
+ const substringArg = args[0];
32
+ if (!substringArg) {
33
+ throw Errors.argumentRequired('lastIndexOf', 'substring argument');
34
+ }
35
+
36
+ const substringResult = await evaluator(substringArg, input, context);
37
+
38
+ if (substringResult.value.length === 0) {
39
+ return { value: [], context };
40
+ }
41
+
42
+ if (substringResult.value.length > 1) {
43
+ throw Errors.singletonRequired('lastIndexOf substring', substringResult.value.length);
44
+ }
45
+
46
+ const boxedSubstring = substringResult.value[0];
47
+ if (!boxedSubstring) {
48
+ return { value: [], context };
49
+ }
50
+
51
+ const substring = unbox(boxedSubstring);
52
+ if (typeof substring !== 'string') {
53
+ throw Errors.invalidStringOperation('lastIndexOf', 'substring argument');
54
+ }
55
+
56
+ // Handle empty substring - returns 0 per spec
57
+ if (substring === '') {
58
+ return { value: [box(0, { type: 'Integer', singleton: true })], context };
59
+ }
60
+
61
+ // Find the last index
62
+ const index = inputValue.lastIndexOf(substring);
63
+
64
+ return { value: [box(index, { type: 'Integer', singleton: true })], context };
65
+ };
66
+
67
+ export const lastIndexOfFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
68
+ name: 'lastIndexOf',
69
+ category: ['string'],
70
+ description: 'Returns the 0-based index of the last position substring is found in the input string, or -1 if it is not found',
71
+ examples: [
72
+ "'abcdefg'.lastIndexOf('bc')",
73
+ "'abcdefg'.lastIndexOf('x')",
74
+ "'abc abc'.lastIndexOf('a')"
75
+ ],
76
+ signatures: [{
77
+ name: 'lastIndexOf',
78
+ input: { type: 'String', singleton: true },
79
+ parameters: [
80
+ { name: 'substring', type: { type: 'String', singleton: true } }
81
+ ],
82
+ result: { type: 'Integer', singleton: true }
83
+ }],
84
+ evaluate
85
+ };
@@ -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
  // Check single item in input
@@ -1,9 +1,8 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { compareQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
+ import { compare } from './comparison';
7
6
 
8
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
8
  if (left.length === 0 || right.length === 0) {
@@ -17,14 +16,14 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
17
16
  if (!boxedr) return { value: [], context };
18
17
  const r = unbox(boxedr);
19
18
 
20
- // Check if both are quantities
21
- if (l && typeof l === 'object' && 'unit' in l &&
22
- r && typeof r === 'object' && 'unit' in r) {
23
- const result = compareQuantities(l as QuantityValue, r as QuantityValue);
24
- return { value: result !== null ? [box(result < 0, { type: 'Boolean', singleton: true })] : [], context };
19
+ // Use the unified compare function which handles all types including FHIR Quantities
20
+ const comparisonResult = compare(l, r);
21
+
22
+ if (comparisonResult.kind === 'incomparable') {
23
+ return { value: [], context };
25
24
  }
26
25
 
27
- return { value: [box((l as any) < (r as any), { type: 'Boolean', singleton: true })], context };
26
+ return { value: [box(comparisonResult.kind === 'less', { type: 'Boolean', singleton: true })], context };
28
27
  };
29
28
 
30
29
  export const lessOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -1,9 +1,8 @@
1
1
  import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
- import { compareQuantities } from '../quantity-value';
5
- import type { QuantityValue } from '../quantity-value';
6
- import { box, unbox } from '../boxing';
4
+ import { box, unbox } from '../interpreter/boxing';
5
+ import { compare } from './comparison';
7
6
 
8
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
9
8
  if (left.length === 0 || right.length === 0) {
@@ -17,14 +16,13 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
17
16
  if (!boxedr) return { value: [], context };
18
17
  const r = unbox(boxedr);
19
18
 
20
- // Check if both are quantities
21
- if (l && typeof l === 'object' && 'unit' in l &&
22
- r && typeof r === 'object' && 'unit' in r) {
23
- const result = compareQuantities(l as QuantityValue, r as QuantityValue);
24
- return { value: result !== null ? [box(result <= 0, { type: 'Boolean', singleton: true })] : [], context };
19
+ const comparisonResult = compare(l, r);
20
+
21
+ if (comparisonResult.kind === 'incomparable') {
22
+ return { value: [], context };
25
23
  }
26
24
 
27
- return { value: [box((l as any) <= (r as any), { type: 'Boolean', singleton: true })], context };
25
+ return { value: [box(comparisonResult.kind === 'less' || comparisonResult.kind === 'equal', { type: 'Boolean', singleton: true })], context };
28
26
  };
29
27
 
30
28
  export const lessOrEqualOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -1,8 +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';
5
- import { compareQuantities, type QuantityValue } from '../quantity-value';
4
+ import { box, unbox } from '../interpreter/boxing';
5
+ import { compare } from './comparison';
6
6
 
7
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
8
8
  if (left.length === 0 || right.length === 0) {
@@ -18,19 +18,14 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
18
18
  const leftValue = unbox(boxedLeft);
19
19
  const rightValue = unbox(boxedRight);
20
20
 
21
- // Handle quantity comparison
22
- if (leftValue && typeof leftValue === 'object' && 'value' in leftValue && 'unit' in leftValue &&
23
- rightValue && typeof rightValue === 'object' && 'value' in rightValue && 'unit' in rightValue) {
24
- const comparison = compareQuantities(leftValue as QuantityValue, rightValue as QuantityValue);
25
- if (comparison === null) {
26
- // Incompatible units - return empty
27
- return { value: [], context };
28
- }
29
- return { value: [box(comparison < 0, { type: 'Boolean', singleton: true })], context };
21
+ // Use the unified compare function which handles all types including FHIR Quantities
22
+ const comparisonResult = compare(leftValue, rightValue);
23
+
24
+ if (comparisonResult.kind === 'incomparable') {
25
+ return { value: [], context };
30
26
  }
31
27
 
32
- // Regular comparison
33
- return { value: [box((leftValue as any) < (rightValue as any), { type: 'Boolean', singleton: true })], context };
28
+ return { value: [box(comparisonResult.kind === 'less', { type: 'Boolean', singleton: true })], context };
34
29
  };
35
30
 
36
31
  export const lessThanOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -0,0 +1,120 @@
1
+ // lowBoundary() function - Returns the least possible value to the specified precision
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import {
5
+ isFHIRDate, isFHIRDateTime, isFHIRTime,
6
+ getDateLowBoundary, getDateTimeLowBoundary, getTimeLowBoundary
7
+ } from '../complex-types/temporal';
8
+ import { getDecimalLowBoundary } from './decimal-boundaries';
9
+ import { Errors } from '../errors';
10
+
11
+ export const lowBoundaryEvaluator: FunctionEvaluator = async (input, context, args, evaluator) => {
12
+ // lowBoundary() takes optional precision parameter
13
+ if (args.length > 1) {
14
+ throw Errors.wrongArgumentCountRange('lowBoundary', 0, 1, args.length);
15
+ }
16
+
17
+ // Empty input returns empty
18
+ if (input.length === 0) {
19
+ return { value: [], context };
20
+ }
21
+
22
+ // Multiple items throws error
23
+ if (input.length > 1) {
24
+ throw Errors.singletonRequired('lowBoundary', input.length);
25
+ }
26
+
27
+ const boxedValue = input[0];
28
+ if (!boxedValue) {
29
+ return { value: [], context };
30
+ }
31
+
32
+ const value = unbox(boxedValue);
33
+
34
+ // Get precision if provided
35
+ let precision: number | undefined;
36
+ if (args.length === 1) {
37
+ const precisionResult = await evaluator(args[0]!, input, context);
38
+ const precisionArg = precisionResult.value;
39
+ if (precisionArg.length === 0) {
40
+ return { value: [], context };
41
+ }
42
+ if (precisionArg.length > 1) {
43
+ throw Errors.singletonRequired('lowBoundary precision', precisionArg.length);
44
+ }
45
+ const precisionValue = unbox(precisionArg[0]!);
46
+ if (typeof precisionValue !== 'number' || !Number.isInteger(precisionValue)) {
47
+ throw Errors.invalidOperandType('lowBoundary precision', typeof precisionValue);
48
+ }
49
+ precision = precisionValue;
50
+ }
51
+
52
+ // Handle Date
53
+ if (isFHIRDate(value)) {
54
+ const result = getDateLowBoundary(value, precision);
55
+ if (!result) {
56
+ return { value: [], context };
57
+ }
58
+ return { value: [box(result, { type: 'Date', singleton: true })], context };
59
+ }
60
+
61
+ // Handle DateTime
62
+ if (isFHIRDateTime(value)) {
63
+ const result = getDateTimeLowBoundary(value, precision);
64
+ if (!result) {
65
+ return { value: [], context };
66
+ }
67
+ return { value: [box(result, { type: 'DateTime', singleton: true })], context };
68
+ }
69
+
70
+ // Handle Time
71
+ if (isFHIRTime(value)) {
72
+ const result = getTimeLowBoundary(value, precision);
73
+ if (!result) {
74
+ return { value: [], context };
75
+ }
76
+ return { value: [box(result, { type: 'Time', singleton: true })], context };
77
+ }
78
+
79
+ // For Decimal/Integer types
80
+ if (typeof value === 'number') {
81
+ const result = getDecimalLowBoundary(value, precision);
82
+ if (result === null) {
83
+ return { value: [], context };
84
+ }
85
+ // Determine the result type based on whether it's an integer or decimal
86
+ const isInteger = Number.isInteger(result);
87
+ return {
88
+ value: [box(result, { type: isInteger ? 'Integer' : 'Decimal', singleton: true })],
89
+ context
90
+ };
91
+ }
92
+
93
+ // Invalid type returns empty
94
+ return { value: [], context };
95
+ };
96
+
97
+ export const lowBoundaryFunction: FunctionDefinition & { evaluate: typeof lowBoundaryEvaluator } = {
98
+ name: 'lowBoundary',
99
+ category: ['utility'],
100
+ description: 'Returns the least possible value of the input to the specified precision',
101
+ examples: [
102
+ '@2014.lowBoundary(6)',
103
+ '@2014-01-01T08.lowBoundary(17)',
104
+ '@T10:30.lowBoundary(9)',
105
+ '1.587.lowBoundary()',
106
+ '1.587.lowBoundary(2)',
107
+ '1.lowBoundary(0)'
108
+ ],
109
+ signatures: [
110
+ {
111
+ name: 'lowBoundary',
112
+ input: { type: 'Any' as const, singleton: true },
113
+ parameters: [
114
+ { name: 'precision', type: { type: 'Integer' as const, singleton: true }, optional: true }
115
+ ],
116
+ result: { type: 'Any' as const, singleton: true }
117
+ }
118
+ ],
119
+ evaluate: lowBoundaryEvaluator
120
+ };
@@ -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
  // Handle empty input collection