@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.
- package/dist/index.browser.d.ts +22 -0
- package/dist/index.browser.js +15758 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.node.d.ts +24 -0
- package/dist/{index.js → index.node.js} +5450 -3809
- package/dist/index.node.js.map +1 -0
- package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
- package/package.json +10 -5
- package/src/analyzer.ts +46 -9
- package/src/complex-types/quantity-value.ts +131 -9
- package/src/complex-types/temporal.ts +45 -6
- package/src/errors.ts +4 -0
- package/src/index.browser.ts +4 -0
- package/src/{index.ts → index.common.ts} +18 -14
- package/src/index.node.ts +4 -0
- package/src/interpreter/navigator.ts +12 -0
- package/src/interpreter/runtime-context.ts +60 -25
- package/src/interpreter.ts +118 -33
- package/src/lexer.ts +4 -1
- package/src/model-provider.browser.ts +35 -0
- package/src/{model-provider.ts → model-provider.common.ts} +29 -26
- package/src/model-provider.node.ts +41 -0
- package/src/operations/allTrue-function.ts +6 -10
- package/src/operations/and-operator.ts +2 -2
- package/src/operations/as-function.ts +41 -0
- package/src/operations/combine-operator.ts +17 -4
- package/src/operations/comparison.ts +73 -21
- package/src/operations/convertsToQuantity-function.ts +56 -7
- package/src/operations/decode-function.ts +114 -0
- package/src/operations/divide-operator.ts +3 -3
- package/src/operations/encode-function.ts +110 -0
- package/src/operations/escape-function.ts +114 -0
- package/src/operations/exp-function.ts +65 -0
- package/src/operations/extension-function.ts +88 -0
- package/src/operations/greater-operator.ts +5 -24
- package/src/operations/greater-or-equal-operator.ts +5 -24
- package/src/operations/hasValue-function.ts +84 -0
- package/src/operations/iif-function.ts +7 -1
- package/src/operations/implies-operator.ts +1 -0
- package/src/operations/index.ts +11 -0
- package/src/operations/is-function.ts +11 -0
- package/src/operations/is-operator.ts +187 -5
- package/src/operations/less-operator.ts +6 -24
- package/src/operations/less-or-equal-operator.ts +5 -24
- package/src/operations/less-than.ts +7 -12
- package/src/operations/ln-function.ts +62 -0
- package/src/operations/log-function.ts +113 -0
- package/src/operations/lowBoundary-function.ts +14 -0
- package/src/operations/minus-operator.ts +8 -1
- package/src/operations/mod-operator.ts +7 -1
- package/src/operations/not-function.ts +9 -2
- package/src/operations/ofType-function.ts +35 -0
- package/src/operations/plus-operator.ts +46 -3
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/replace-function.ts +19 -19
- package/src/operations/replaceMatches-function.ts +5 -0
- package/src/operations/sort-function.ts +209 -0
- package/src/operations/take-function.ts +1 -1
- package/src/operations/toQuantity-function.ts +0 -1
- package/src/operations/toString-function.ts +76 -12
- package/src/operations/trace-function.ts +20 -3
- package/src/operations/unescape-function.ts +119 -0
- package/src/operations/where-function.ts +3 -1
- package/src/parser.ts +14 -2
- package/src/types.ts +7 -5
- package/src/utils/decimal.ts +76 -0
- 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,
|
|
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(
|
|
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
|
-
//
|
|
54
|
-
|
|
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
|
-
//
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
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
|