@atomic-ehr/fhirpath 0.0.3 → 0.0.4

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 (67) hide show
  1. package/dist/index.browser.d.ts +22 -0
  2. package/dist/index.browser.js +15758 -0
  3. package/dist/index.browser.js.map +1 -0
  4. package/dist/index.node.d.ts +24 -0
  5. package/dist/{index.js → index.node.js} +5450 -3809
  6. package/dist/index.node.js.map +1 -0
  7. package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
  8. package/package.json +10 -5
  9. package/src/analyzer.ts +46 -9
  10. package/src/complex-types/quantity-value.ts +131 -9
  11. package/src/complex-types/temporal.ts +45 -6
  12. package/src/errors.ts +4 -0
  13. package/src/index.browser.ts +4 -0
  14. package/src/{index.ts → index.common.ts} +18 -14
  15. package/src/index.node.ts +4 -0
  16. package/src/interpreter/navigator.ts +12 -0
  17. package/src/interpreter/runtime-context.ts +60 -25
  18. package/src/interpreter.ts +118 -33
  19. package/src/lexer.ts +4 -1
  20. package/src/model-provider.browser.ts +35 -0
  21. package/src/{model-provider.ts → model-provider.common.ts} +29 -26
  22. package/src/model-provider.node.ts +41 -0
  23. package/src/operations/allTrue-function.ts +6 -10
  24. package/src/operations/and-operator.ts +2 -2
  25. package/src/operations/as-function.ts +41 -0
  26. package/src/operations/combine-operator.ts +17 -4
  27. package/src/operations/comparison.ts +73 -21
  28. package/src/operations/convertsToQuantity-function.ts +56 -7
  29. package/src/operations/decode-function.ts +114 -0
  30. package/src/operations/divide-operator.ts +3 -3
  31. package/src/operations/encode-function.ts +110 -0
  32. package/src/operations/escape-function.ts +114 -0
  33. package/src/operations/exp-function.ts +65 -0
  34. package/src/operations/extension-function.ts +88 -0
  35. package/src/operations/greater-operator.ts +5 -24
  36. package/src/operations/greater-or-equal-operator.ts +5 -24
  37. package/src/operations/hasValue-function.ts +84 -0
  38. package/src/operations/iif-function.ts +7 -1
  39. package/src/operations/implies-operator.ts +1 -0
  40. package/src/operations/index.ts +11 -0
  41. package/src/operations/is-function.ts +11 -0
  42. package/src/operations/is-operator.ts +187 -5
  43. package/src/operations/less-operator.ts +6 -24
  44. package/src/operations/less-or-equal-operator.ts +5 -24
  45. package/src/operations/less-than.ts +7 -12
  46. package/src/operations/ln-function.ts +62 -0
  47. package/src/operations/log-function.ts +113 -0
  48. package/src/operations/lowBoundary-function.ts +14 -0
  49. package/src/operations/minus-operator.ts +8 -1
  50. package/src/operations/mod-operator.ts +7 -1
  51. package/src/operations/not-function.ts +9 -2
  52. package/src/operations/ofType-function.ts +35 -0
  53. package/src/operations/plus-operator.ts +46 -3
  54. package/src/operations/precision-function.ts +146 -0
  55. package/src/operations/replace-function.ts +19 -19
  56. package/src/operations/replaceMatches-function.ts +5 -0
  57. package/src/operations/sort-function.ts +209 -0
  58. package/src/operations/take-function.ts +1 -1
  59. package/src/operations/toQuantity-function.ts +0 -1
  60. package/src/operations/toString-function.ts +76 -12
  61. package/src/operations/trace-function.ts +20 -3
  62. package/src/operations/unescape-function.ts +119 -0
  63. package/src/operations/where-function.ts +3 -1
  64. package/src/parser.ts +14 -2
  65. package/src/types.ts +7 -5
  66. package/src/utils/decimal.ts +76 -0
  67. package/dist/index.js.map +0 -1
@@ -14,9 +14,162 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
14
14
  const boxedItem = left[0];
15
15
  const item = unbox(boxedItem);
16
16
  const typeName = right[0] as string; // Should be a type name like 'String', 'Integer', etc.
17
-
18
17
  // If we have a ModelProvider and typeInfo, use it for accurate type checking (handles subtypes)
19
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
+
62
+ // Check type hierarchy using schema if available
63
+ if (context.modelProvider && boxedItem.typeInfo.name) {
64
+ // Check if the item's type is derived from the check type
65
+ const itemSchema = await (context.modelProvider as any).getSchema(boxedItem.typeInfo.name);
66
+ if (itemSchema && itemSchema.base) {
67
+ // Extract the type name from the base URL
68
+ const baseTypeName = itemSchema.base.split('/').pop();
69
+ if (baseTypeName === checkType) {
70
+ return {
71
+ value: [box(true, { type: 'Boolean', singleton: true })],
72
+ context
73
+ };
74
+ }
75
+ // Check recursively up the hierarchy
76
+ let currentBase = baseTypeName;
77
+ while (currentBase) {
78
+ if (currentBase === checkType) {
79
+ return {
80
+ value: [box(true, { type: 'Boolean', singleton: true })],
81
+ context
82
+ };
83
+ }
84
+ const baseSchema = await (context.modelProvider as any).getSchema(currentBase);
85
+ if (!baseSchema || !baseSchema.base) break;
86
+ currentBase = baseSchema.base.split('/').pop();
87
+ }
88
+ }
89
+ }
90
+
91
+ // Also check if asking for a resource type like FHIR.Patient
92
+ if (boxedItem.typeInfo.type === checkType || boxedItem.typeInfo.name === checkType) {
93
+ return {
94
+ value: [box(true, { type: 'Boolean', singleton: true })],
95
+ context
96
+ };
97
+ }
98
+ }
99
+ return {
100
+ value: [box(false, { type: 'Boolean', singleton: true })],
101
+ context
102
+ };
103
+ }
104
+
105
+ // When no namespace is specified (e.g., just "Boolean", "String", "string", "code")
106
+ if (!checkNamespace) {
107
+ // Check for System primitive types (capitalized)
108
+ const systemPrimitiveTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
109
+ if (systemPrimitiveTypes.includes(checkType)) {
110
+ // For System primitive types, only match if NOT a FHIR type
111
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
112
+ return {
113
+ value: [box(false, { type: 'Boolean', singleton: true })],
114
+ context
115
+ };
116
+ }
117
+ // Check if the type matches
118
+ if (boxedItem.typeInfo.type === checkType) {
119
+ return {
120
+ value: [box(true, { type: 'Boolean', singleton: true })],
121
+ context
122
+ };
123
+ }
124
+ }
125
+
126
+ // Check for FHIR primitive types (lowercase) like string, code, id, etc.
127
+ const fhirPrimitiveTypes = ['string', 'code', 'id', 'uri', 'url', 'canonical', 'uuid', 'oid',
128
+ 'boolean', 'integer', 'decimal', 'base64Binary', 'instant',
129
+ 'date', 'dateTime', 'time', 'unsignedInt', 'positiveInt', 'markdown'];
130
+ if (fhirPrimitiveTypes.includes(checkType)) {
131
+ // For FHIR primitive types, check both the name and underlying type
132
+ if (boxedItem.typeInfo.namespace === 'FHIR') {
133
+ // Check if the name matches
134
+ if (boxedItem.typeInfo.name === checkType) {
135
+ return {
136
+ value: [box(true, { type: 'Boolean', singleton: true })],
137
+ context
138
+ };
139
+ }
140
+ // For 'string', also match code, id, etc. (they are all string-based)
141
+ if (checkType === 'string') {
142
+ const stringBasedTypes = ['code', 'id', 'uri', 'url', 'canonical', 'uuid', 'oid', 'markdown'];
143
+ if (boxedItem.typeInfo.name && stringBasedTypes.includes(boxedItem.typeInfo.name)) {
144
+ return {
145
+ value: [box(true, { type: 'Boolean', singleton: true })],
146
+ context
147
+ };
148
+ }
149
+ // Also check if underlying type is String
150
+ if (boxedItem.typeInfo.type === 'String') {
151
+ return {
152
+ value: [box(true, { type: 'Boolean', singleton: true })],
153
+ context
154
+ };
155
+ }
156
+ }
157
+ }
158
+ return {
159
+ value: [box(false, { type: 'Boolean', singleton: true })],
160
+ context
161
+ };
162
+ }
163
+
164
+ // For non-primitive types (like Patient, Observation), use model provider
165
+ const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as TypeName);
166
+ return {
167
+ value: [box(matchingType !== undefined, { type: 'Boolean', singleton: true })],
168
+ context
169
+ };
170
+ }
171
+
172
+ // Default case - use model provider's ofType for other checks
20
173
  const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as TypeName);
21
174
  return {
22
175
  value: [box(matchingType !== undefined, { type: 'Boolean', singleton: true })],
@@ -24,10 +177,24 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
24
177
  };
25
178
  }
26
179
 
27
- // Check if the box has type information (without ModelProvider, just check exact match)
180
+ // Check if the box has type information (without ModelProvider, check with normalization)
28
181
  if (boxedItem?.typeInfo) {
182
+ // Normalize both types for comparison
183
+ let boxedType = boxedItem.typeInfo.type;
184
+ let compareType = typeName;
185
+
186
+ // Normalize System.X to X for primitive types
187
+ if (compareType.startsWith('System.')) {
188
+ compareType = compareType.substring(7);
189
+ }
190
+
191
+ // Also check if boxedType needs normalization (unlikely but consistent)
192
+ if (boxedType.startsWith('System.')) {
193
+ boxedType = boxedType.substring(7);
194
+ }
195
+
29
196
  return {
30
- value: [box(boxedItem.typeInfo.type === typeName, { type: 'Boolean', singleton: true })],
197
+ value: [box(boxedType === compareType, { type: 'Boolean', singleton: true })],
31
198
  context
32
199
  };
33
200
  }
@@ -50,15 +217,30 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
50
217
  };
51
218
  }
52
219
 
53
- // Check primitive types
54
- switch (typeName) {
220
+ // Normalize type names - strip namespace for System types
221
+ // System.Boolean -> Boolean, FHIR.boolean -> boolean, etc.
222
+ let normalizedTypeName = typeName;
223
+ if (typeName.startsWith('System.')) {
224
+ normalizedTypeName = typeName.substring(7); // Remove "System."
225
+ } else if (typeName.startsWith('FHIR.')) {
226
+ // For FHIR types, keep the namespace for model provider lookup
227
+ // but also check without namespace for primitive types
228
+ normalizedTypeName = typeName;
229
+ }
230
+
231
+ // Check primitive types (with normalized names)
232
+ switch (normalizedTypeName) {
55
233
  case 'String':
234
+ case 'System.String':
56
235
  return { value: [box(typeof item === 'string', { type: 'Boolean', singleton: true })], context };
57
236
  case 'Boolean':
237
+ case 'System.Boolean':
58
238
  return { value: [box(typeof item === 'boolean', { type: 'Boolean', singleton: true })], context };
59
239
  case 'Integer':
240
+ case 'System.Integer':
60
241
  return { value: [box(typeof item === 'number' && Number.isInteger(item), { type: 'Boolean', singleton: true })], context };
61
242
  case 'Decimal':
243
+ case 'System.Decimal':
62
244
  return { value: [box(typeof item === 'number', { type: 'Boolean', singleton: true })], context };
63
245
  case 'Date':
64
246
  // Check if it's a FHIRDate instance or has Date type
@@ -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 '../complex-types/quantity-value';
5
- import type { QuantityValue } from '../complex-types/quantity-value';
6
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,31 +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 };
25
- }
19
+ // Use the unified compare function which handles all types including FHIR Quantities
20
+ const comparisonResult = compare(l, r);
26
21
 
27
- // Check if both are temporal values (Date, DateTime, Time)
28
- if (l && typeof l === 'object' && 'kind' in l &&
29
- r && typeof r === 'object' && 'kind' in r) {
30
- const temporalL = l as any;
31
- const temporalR = r as any;
32
- const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
- if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
- const { compare } = await import('../complex-types/temporal');
35
- const result = compare(temporalL, temporalR);
36
- // null means incomparable (different precisions), returns empty
37
- if (result === null) {
38
- return { value: [], context };
39
- }
40
- return { value: [box(result < 0, { type: 'Boolean', singleton: true })], context };
41
- }
22
+ if (comparisonResult.kind === 'incomparable') {
23
+ return { value: [], context };
42
24
  }
43
25
 
44
- 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 };
45
27
  };
46
28
 
47
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 '../complex-types/quantity-value';
5
- import type { QuantityValue } from '../complex-types/quantity-value';
6
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,31 +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 };
25
- }
19
+ const comparisonResult = compare(l, r);
26
20
 
27
- // Check if both are temporal values (Date, DateTime, Time)
28
- if (l && typeof l === 'object' && 'kind' in l &&
29
- r && typeof r === 'object' && 'kind' in r) {
30
- const temporalL = l as any;
31
- const temporalR = r as any;
32
- const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
- if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
- const { compare } = await import('../complex-types/temporal');
35
- const result = compare(temporalL, temporalR);
36
- // null means incomparable (different precisions), returns empty
37
- if (result === null) {
38
- return { value: [], context };
39
- }
40
- return { value: [box(result <= 0, { type: 'Boolean', singleton: true })], context };
41
- }
21
+ if (comparisonResult.kind === 'incomparable') {
22
+ return { value: [], context };
42
23
  }
43
24
 
44
- 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 };
45
26
  };
46
27
 
47
28
  export const lessOrEqualOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
@@ -2,7 +2,7 @@ import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
4
  import { box, unbox } from '../interpreter/boxing';
5
- import { compareQuantities, type QuantityValue } from '../complex-types/quantity-value';
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,62 @@
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
+ // ln() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('ln', 0, args.length);
9
+ }
10
+
11
+ // If input is empty, return empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input has multiple items, error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('ln', input.length);
19
+ }
20
+
21
+ const boxedValue = input[0];
22
+ if (!boxedValue) return { value: [], context };
23
+ const value = unbox(boxedValue);
24
+
25
+ // Must be a number
26
+ if (typeof value !== 'number') {
27
+ throw Errors.invalidOperandType('ln', `${typeof value}`);
28
+ }
29
+
30
+ // If zero or negative, return empty (ln is undefined for non-positive numbers)
31
+ if (value <= 0) {
32
+ return { value: [], context };
33
+ }
34
+
35
+ return { value: [box(Math.log(value), { type: 'Decimal', singleton: true })], context };
36
+ };
37
+
38
+ export const lnFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
39
+ name: 'ln',
40
+ category: ['math'],
41
+ description: 'Returns the natural logarithm (base e) of the input number as a Decimal.',
42
+ examples: [
43
+ '1.ln()',
44
+ '2.71828.ln()',
45
+ '(-1).ln()'
46
+ ],
47
+ signatures: [
48
+ {
49
+ name: 'ln-integer',
50
+ input: { type: 'Integer', singleton: true },
51
+ parameters: [],
52
+ result: { type: 'Decimal', singleton: true }
53
+ },
54
+ {
55
+ name: 'ln-decimal',
56
+ input: { type: 'Decimal', singleton: true },
57
+ parameters: [],
58
+ result: { type: 'Decimal', singleton: true }
59
+ }
60
+ ],
61
+ evaluate
62
+ };
@@ -0,0 +1,113 @@
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
+ // log() takes exactly one argument (base)
7
+ if (args.length !== 1) {
8
+ throw Errors.wrongArgumentCount('log', 1, args.length);
9
+ }
10
+
11
+ // If input is empty, return empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input has multiple items, error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('log', input.length);
19
+ }
20
+
21
+ const boxedNumber = input[0];
22
+ if (!boxedNumber) {
23
+ return { value: [], context };
24
+ }
25
+
26
+ const number = unbox(boxedNumber);
27
+
28
+ // Number must be a number
29
+ if (typeof number !== 'number') {
30
+ throw Errors.invalidOperandType('log', `${typeof number}`);
31
+ }
32
+
33
+ // Evaluate base
34
+ const baseResult = await evaluator(args[0]!, input, context);
35
+ if (baseResult.value.length === 0) {
36
+ return { value: [], context };
37
+ }
38
+ if (baseResult.value.length > 1) {
39
+ throw Errors.invalidOperation('log base must be a single value');
40
+ }
41
+
42
+ const boxedBase = baseResult.value[0];
43
+ if (!boxedBase) {
44
+ return { value: [], context };
45
+ }
46
+
47
+ const base = unbox(boxedBase);
48
+ if (typeof base !== 'number') {
49
+ throw Errors.invalidOperation('log base must be a number');
50
+ }
51
+
52
+ // Check for invalid inputs
53
+ if (number <= 0 || base <= 0 || base === 1) {
54
+ // Logarithm undefined for non-positive numbers or base 1
55
+ return { value: [], context };
56
+ }
57
+
58
+ // Calculate logarithm using change of base formula: log_b(x) = ln(x) / ln(b)
59
+ const result = Math.log(number) / Math.log(base);
60
+
61
+ // Check if result is valid (not NaN or Infinity)
62
+ if (!isFinite(result)) {
63
+ return { value: [], context };
64
+ }
65
+
66
+ // Always return as Decimal per spec
67
+ return { value: [box(result, { type: 'Decimal', singleton: true })], context };
68
+ };
69
+
70
+ export const logFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
71
+ name: 'log',
72
+ category: ['math'],
73
+ description: 'Returns the logarithm base base of the input number. When used with Integers, the arguments will be implicitly converted to Decimal.',
74
+ examples: [
75
+ '16.log(2)',
76
+ '100.0.log(10.0)'
77
+ ],
78
+ signatures: [
79
+ {
80
+ name: 'log-integer',
81
+ input: { type: 'Integer', singleton: true },
82
+ parameters: [
83
+ { name: 'base', type: { type: 'Integer', singleton: true }, optional: false }
84
+ ],
85
+ result: { type: 'Decimal', singleton: true }
86
+ },
87
+ {
88
+ name: 'log-decimal',
89
+ input: { type: 'Decimal', singleton: true },
90
+ parameters: [
91
+ { name: 'base', type: { type: 'Decimal', singleton: true }, optional: false }
92
+ ],
93
+ result: { type: 'Decimal', singleton: true }
94
+ },
95
+ {
96
+ name: 'log-integer-decimal',
97
+ input: { type: 'Integer', singleton: true },
98
+ parameters: [
99
+ { name: 'base', type: { type: 'Decimal', singleton: true }, optional: false }
100
+ ],
101
+ result: { type: 'Decimal', singleton: true }
102
+ },
103
+ {
104
+ name: 'log-decimal-integer',
105
+ input: { type: 'Decimal', singleton: true },
106
+ parameters: [
107
+ { name: 'base', type: { type: 'Integer', singleton: true }, optional: false }
108
+ ],
109
+ result: { type: 'Decimal', singleton: true }
110
+ }
111
+ ],
112
+ evaluate
113
+ };
@@ -90,6 +90,20 @@ export const lowBoundaryEvaluator: FunctionEvaluator = async (input, context, ar
90
90
  };
91
91
  }
92
92
 
93
+ // Handle Quantity type
94
+ if (value && typeof value === 'object' && 'value' in value && 'unit' in value) {
95
+ const quantity = value as { value: number; unit: string };
96
+ const result = getDecimalLowBoundary(quantity.value, precision);
97
+ if (result === null) {
98
+ return { value: [], context };
99
+ }
100
+ // Return a new Quantity with the adjusted value and same unit
101
+ return {
102
+ value: [box({ value: result, unit: quantity.unit }, { type: 'Quantity', singleton: true })],
103
+ context
104
+ };
105
+ }
106
+
93
107
  // Invalid type returns empty
94
108
  return { value: [], context };
95
109
  };
@@ -4,6 +4,7 @@ import type { OperationEvaluator } from '../types';
4
4
  import { subtractQuantities } from '../complex-types/quantity-value';
5
5
  import type { QuantityValue } from '../complex-types/quantity-value';
6
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) {
@@ -87,7 +88,13 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
87
88
 
88
89
  // Handle numeric subtraction
89
90
  if (typeof l === 'number' && typeof r === 'number') {
90
- const result = l - r;
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
+
91
98
  const typeInfo = Number.isInteger(result) ?
92
99
  { type: 'Integer' as const, singleton: true } :
93
100
  { type: 'Decimal' as const, singleton: true };
@@ -2,6 +2,7 @@ import type { OperatorDefinition } from '../types';
2
2
  import { PRECEDENCE } from '../types';
3
3
  import type { OperationEvaluator } from '../types';
4
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';
@@ -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 } = {
@@ -44,6 +44,41 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
44
44
  throw Errors.invalidOperation(`ofType() requires a type name as argument, got ${typeArg.type}`);
45
45
  }
46
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
+
47
82
  // If we have typeInfo from the analyzer (with ModelProvider), use it
48
83
  // NOTE: This optimization is currently disabled because currentNode refers to the ofType
49
84
  // function node, not the input navigation node. The correct type checking happens below