@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.
- package/README.md +716 -238
- package/dist/index.d.ts +226 -120
- package/dist/index.js +11552 -5580
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +939 -1204
- package/src/completion-provider.ts +209 -191
- package/src/complex-types/quantity-value.ts +410 -0
- package/src/complex-types/temporal.ts +1776 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +506 -468
- package/src/lexer.ts +192 -211
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +99 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +744 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +132 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/extension-function.ts +84 -0
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +7 -9
- package/src/operations/greater-or-equal-operator.ts +7 -9
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +193 -8
- package/src/operations/implies-operator.ts +2 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +43 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +70 -0
- package/src/operations/is-operator.ts +176 -13
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +8 -9
- package/src/operations/less-or-equal-operator.ts +7 -9
- package/src/operations/less-than.ts +8 -13
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +76 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +8 -2
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +10 -3
- package/src/operations/ofType-function.ts +43 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +125 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +78 -15
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +262 -503
- package/src/registry.ts +53 -42
- package/src/types.ts +129 -17
- package/src/utils/decimal.ts +76 -0
- package/src/utils/pprint.ts +151 -0
- package/src/quantity-value.ts +0 -198
|
@@ -1,64 +1,23 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box
|
|
4
|
+
import { box } from '../interpreter/boxing';
|
|
5
|
+
import { collectionsNotEquivalent } from './comparison';
|
|
5
6
|
|
|
6
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
return { value: [box(false, { type: 'Boolean', singleton: true })], context };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Different sizes are not equivalent, so not-equivalent returns true
|
|
13
|
-
if (left.length !== right.length) {
|
|
14
|
-
return { value: [box(true, { type: 'Boolean', singleton: true })], context };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// For single items, check type-specific non-equivalence
|
|
18
|
-
if (left.length === 1 && right.length === 1) {
|
|
19
|
-
const boxedl = left[0];
|
|
20
|
-
if (!boxedl) return { value: [], context };
|
|
21
|
-
const l = unbox(boxedl);
|
|
22
|
-
const boxedr = right[0];
|
|
23
|
-
if (!boxedr) return { value: [], context };
|
|
24
|
-
const r = unbox(boxedr);
|
|
25
|
-
|
|
26
|
-
// String non-equivalence: case-insensitive with normalized whitespace
|
|
27
|
-
if (typeof l === 'string' && typeof r === 'string') {
|
|
28
|
-
const normalizeString = (s: string) => s.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
29
|
-
return { value: [box(normalizeString(l) !== normalizeString(r), { type: 'Boolean', singleton: true })], context };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Number non-equivalence (for Integer and Decimal)
|
|
33
|
-
if (typeof l === 'number' && typeof r === 'number') {
|
|
34
|
-
// For decimals with different precision, round to least precise
|
|
35
|
-
// This is a simplified implementation
|
|
36
|
-
return { value: [box(Math.abs(l - r) >= Number.EPSILON, { type: 'Boolean', singleton: true })], context };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Boolean non-equivalence
|
|
40
|
-
if (typeof l === 'boolean' && typeof r === 'boolean') {
|
|
41
|
-
return { value: [box(l !== r, { type: 'Boolean', singleton: true })], context };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// For complex types and other cases, use inequality for now
|
|
45
|
-
// TODO: Implement full non-equivalence logic for Date/DateTime/Time and complex types
|
|
46
|
-
return { value: [box(l !== r, { type: 'Boolean', singleton: true })], context };
|
|
47
|
-
}
|
|
8
|
+
// Use the new collectionsNotEquivalent function from comparison.ts
|
|
9
|
+
const result = collectionsNotEquivalent(left, right);
|
|
48
10
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const leftSorted = [...left].sort();
|
|
53
|
-
const rightSorted = [...right].sort();
|
|
54
|
-
|
|
55
|
-
for (let i = 0; i < leftSorted.length; i++) {
|
|
56
|
-
if (leftSorted[i] !== rightSorted[i]) {
|
|
57
|
-
return { value: [box(true, { type: 'Boolean', singleton: true })], context };
|
|
58
|
-
}
|
|
11
|
+
// null result means incomparable - return empty collection
|
|
12
|
+
if (result === null) {
|
|
13
|
+
return { value: [], context };
|
|
59
14
|
}
|
|
60
15
|
|
|
61
|
-
|
|
16
|
+
// Return boolean result
|
|
17
|
+
return {
|
|
18
|
+
value: [box(result, { type: 'Boolean', singleton: true })],
|
|
19
|
+
context
|
|
20
|
+
};
|
|
62
21
|
};
|
|
63
22
|
|
|
64
23
|
export const notEquivalentOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -67,6 +26,7 @@ export const notEquivalentOperator: OperatorDefinition & { evaluate: OperationEv
|
|
|
67
26
|
category: ['equality'],
|
|
68
27
|
precedence: PRECEDENCE.EQUALITY,
|
|
69
28
|
associativity: 'left',
|
|
29
|
+
doesNotPropagateEmpty: true, // Empty collections are valid operands for not-equivalence
|
|
70
30
|
description: 'The converse of the equivalent operator, returning true if equivalent returns false and false if equivalent returns true. In other words, A !~ B is short-hand for (A ~ B).not()',
|
|
71
31
|
examples: [
|
|
72
32
|
"'abc' !~ 'ABC'",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition } from '../types';
|
|
2
2
|
import type { FunctionEvaluator } from '../types';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// Three-valued logic implementation
|
|
@@ -9,6 +9,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
9
9
|
return { value: [], context };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Multiple values should throw an error per XML tests
|
|
13
|
+
if (input.length > 1) {
|
|
14
|
+
throw new Error('not() can only be applied to a single item');
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
const boxedValue = input[0];
|
|
13
18
|
if (!boxedValue) {
|
|
14
19
|
return { value: [], context };
|
|
@@ -24,8 +29,10 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
24
29
|
return { value: [box(true, { type: 'Boolean', singleton: true })], context };
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
// Non-boolean values
|
|
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 } = {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { FunctionDefinition, RuntimeContext, ASTNode, TypeInfo, NodeEvaluator, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import type { FHIRPathValue } from '../boxing';
|
|
4
|
-
import { unbox } from '../boxing';
|
|
3
|
+
import type { FHIRPathValue } from '../interpreter/boxing';
|
|
4
|
+
import { unbox } from '../interpreter/boxing';
|
|
5
5
|
import { isIdentifierNode, isFunctionNode } from '../types';
|
|
6
|
-
import { NodeType } from '../types';
|
|
7
6
|
|
|
8
7
|
export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
9
8
|
name: 'ofType',
|
|
@@ -21,7 +20,8 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
21
20
|
{
|
|
22
21
|
name: 'type',
|
|
23
22
|
type: { type: 'Any', singleton: true },
|
|
24
|
-
expression: true
|
|
23
|
+
expression: true,
|
|
24
|
+
typeReference: true // This parameter expects a type name
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
result: 'inputType'
|
|
@@ -37,10 +37,6 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
37
37
|
let targetTypeName: string;
|
|
38
38
|
if (isIdentifierNode(typeArg)) {
|
|
39
39
|
targetTypeName = typeArg.name;
|
|
40
|
-
} else if (typeArg.type === NodeType.TypeOrIdentifier) {
|
|
41
|
-
targetTypeName = (typeArg as any).name;
|
|
42
|
-
} else if (typeArg.type === NodeType.TypeReference) {
|
|
43
|
-
targetTypeName = (typeArg as any).name;
|
|
44
40
|
} else if (isFunctionNode(typeArg) && isIdentifierNode(typeArg.name)) {
|
|
45
41
|
// Handle cases like ofType(Patient())
|
|
46
42
|
targetTypeName = typeArg.name.name;
|
|
@@ -48,6 +44,41 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
48
44
|
throw Errors.invalidOperation(`ofType() requires a type name as argument, got ${typeArg.type}`);
|
|
49
45
|
}
|
|
50
46
|
|
|
47
|
+
// Validate the type name using ModelProvider if available
|
|
48
|
+
if (context.modelProvider) {
|
|
49
|
+
// Try to get the type from the model provider
|
|
50
|
+
const typeInfo = await context.modelProvider.getType(targetTypeName);
|
|
51
|
+
if (!typeInfo) {
|
|
52
|
+
// Not a valid FHIR type, check if it's a System type
|
|
53
|
+
const systemTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
54
|
+
const fhirPrimitiveTypes = ['boolean', 'integer', 'string', 'decimal', 'uri', 'url', 'canonical',
|
|
55
|
+
'base64Binary', 'instant', 'date', 'dateTime', 'time', 'code', 'oid',
|
|
56
|
+
'id', 'markdown', 'unsignedInt', 'positiveInt', 'uuid', 'xhtml'];
|
|
57
|
+
|
|
58
|
+
if (!systemTypes.includes(targetTypeName) && !fhirPrimitiveTypes.includes(targetTypeName)) {
|
|
59
|
+
throw Errors.invalidOperation(`Unknown type: ${targetTypeName}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Without ModelProvider, only allow basic System types and reject obvious invalid names
|
|
64
|
+
const systemTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
|
|
65
|
+
const fhirPrimitiveTypes = ['boolean', 'integer', 'string', 'decimal', 'uri', 'url', 'canonical',
|
|
66
|
+
'base64Binary', 'instant', 'date', 'dateTime', 'time', 'code', 'oid',
|
|
67
|
+
'id', 'markdown', 'unsignedInt', 'positiveInt', 'uuid'];
|
|
68
|
+
|
|
69
|
+
// If it's not a known primitive type and looks invalid, reject it
|
|
70
|
+
if (!systemTypes.includes(targetTypeName) && !fhirPrimitiveTypes.includes(targetTypeName)) {
|
|
71
|
+
// Check if it looks like a valid type name
|
|
72
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(targetTypeName) && !/^[a-z][a-z0-9]*$/i.test(targetTypeName)) {
|
|
73
|
+
throw Errors.invalidOperation(`Invalid type name: ${targetTypeName}`);
|
|
74
|
+
}
|
|
75
|
+
// If it contains numbers but isn't a known type, it's likely invalid
|
|
76
|
+
if (/\d/.test(targetTypeName) && targetTypeName !== 'base64Binary') {
|
|
77
|
+
throw Errors.invalidOperation(`Unknown type: ${targetTypeName}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
// If we have typeInfo from the analyzer (with ModelProvider), use it
|
|
52
83
|
// NOTE: This optimization is currently disabled because currentNode refers to the ofType
|
|
53
84
|
// function node, not the input navigation node. The correct type checking happens below
|
|
@@ -91,9 +122,9 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
91
122
|
return matchingType !== undefined;
|
|
92
123
|
}
|
|
93
124
|
|
|
94
|
-
// Check if the box has type information
|
|
95
|
-
if (boxedItem.typeInfo) {
|
|
96
|
-
// If we have type info, use it for accurate filtering
|
|
125
|
+
// Check if the box has specific type information (not just "Any")
|
|
126
|
+
if (boxedItem.typeInfo && boxedItem.typeInfo.type !== 'Any') {
|
|
127
|
+
// If we have specific type info, use it for accurate filtering
|
|
97
128
|
return boxedItem.typeInfo.type === targetTypeName;
|
|
98
129
|
}
|
|
99
130
|
|
|
@@ -136,4 +167,4 @@ export const ofTypeFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
136
167
|
|
|
137
168
|
return { value: actualFiltered, context };
|
|
138
169
|
}
|
|
139
|
-
};
|
|
170
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
7
|
// Three-valued logic implementation
|
|
@@ -30,6 +30,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
30
30
|
export const orOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
31
31
|
symbol: 'or',
|
|
32
32
|
name: 'or',
|
|
33
|
+
doesNotPropagateEmpty: true, // Three-valued logic: true or empty = true
|
|
33
34
|
category: ['logical'],
|
|
34
35
|
precedence: PRECEDENCE.OR,
|
|
35
36
|
associativity: 'left',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { addQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { addQuantities } from '../complex-types/quantity-value';
|
|
5
|
+
import type { QuantityValue } from '../complex-types/quantity-value';
|
|
6
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
7
7
|
|
|
8
8
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
9
9
|
if (left.length === 0 || right.length === 0) {
|
|
@@ -20,6 +20,69 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
20
20
|
const l = unbox(boxedL);
|
|
21
21
|
const r = unbox(boxedR);
|
|
22
22
|
|
|
23
|
+
// Check for temporal arithmetic using boxed type information
|
|
24
|
+
const leftType = boxedL?.typeInfo?.type;
|
|
25
|
+
const rightType = boxedR?.typeInfo?.type;
|
|
26
|
+
|
|
27
|
+
if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
|
|
28
|
+
// Left is temporal, right is quantity
|
|
29
|
+
const temporalType = leftType;
|
|
30
|
+
const temporal = l;
|
|
31
|
+
const quantity = r as QuantityValue;
|
|
32
|
+
|
|
33
|
+
// Import temporal utilities and create TimeQuantity
|
|
34
|
+
const { createTimeQuantity, add } = await import('../complex-types/temporal');
|
|
35
|
+
|
|
36
|
+
// Calendar duration units (allowed for temporal arithmetic)
|
|
37
|
+
const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
|
|
38
|
+
'day', 'days', 'hour', 'hours', 'minute', 'minutes',
|
|
39
|
+
'second', 'seconds', 'millisecond', 'milliseconds'];
|
|
40
|
+
|
|
41
|
+
// Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
|
|
42
|
+
const variableDurationUnits = ['a', 'mo'];
|
|
43
|
+
|
|
44
|
+
// Fixed duration UCUM units (allowed - they map directly to calendar units)
|
|
45
|
+
const fixedDurationUnitMap: Record<string, string> = {
|
|
46
|
+
'd': 'day',
|
|
47
|
+
'wk': 'week',
|
|
48
|
+
'h': 'hour',
|
|
49
|
+
'min': 'minute',
|
|
50
|
+
's': 'second',
|
|
51
|
+
'ms': 'millisecond'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Check if this is a variable duration unit (not allowed)
|
|
55
|
+
if (variableDurationUnits.includes(quantity.unit)) {
|
|
56
|
+
// Variable duration units like 'a' and 'mo' cannot be added to temporal values
|
|
57
|
+
// because they don't have fixed durations
|
|
58
|
+
const { Errors } = await import('../errors');
|
|
59
|
+
throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Map fixed duration UCUM units to calendar units
|
|
63
|
+
let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
|
|
64
|
+
|
|
65
|
+
// Check if this is a valid calendar duration unit (after mapping)
|
|
66
|
+
if (!calendarUnits.includes(mappedUnit)) {
|
|
67
|
+
// Non-time units with temporal values return empty per FHIRPath spec
|
|
68
|
+
return { value: [], context };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
|
|
72
|
+
|
|
73
|
+
// Use the functional add operation
|
|
74
|
+
const result = add(temporal as any, timeQuantity);
|
|
75
|
+
|
|
76
|
+
if (temporalType === 'Date') {
|
|
77
|
+
return { value: [box(result, { type: 'Date', singleton: true })], context };
|
|
78
|
+
} else if (temporalType === 'DateTime') {
|
|
79
|
+
return { value: [box(result, { type: 'DateTime', singleton: true })], context };
|
|
80
|
+
} else if (temporalType === 'Time') {
|
|
81
|
+
// Let the error propagate - adding calendar units to Time should throw
|
|
82
|
+
return { value: [box(result, { type: 'Time', singleton: true })], context };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
// Check if both are quantities
|
|
24
87
|
if (l && typeof l === 'object' && 'unit' in l &&
|
|
25
88
|
r && typeof r === 'object' && 'unit' in r) {
|
|
@@ -27,8 +90,9 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
27
90
|
return { value: result ? [box(result, { type: 'Quantity', singleton: true })] : [], context };
|
|
28
91
|
}
|
|
29
92
|
|
|
30
|
-
|
|
31
|
-
|
|
93
|
+
// String concatenation only works for string + string
|
|
94
|
+
if (typeof l === 'string' && typeof r === 'string') {
|
|
95
|
+
return { value: [box(l + r, { type: 'String', singleton: true })], context };
|
|
32
96
|
}
|
|
33
97
|
|
|
34
98
|
if (typeof l === 'number' && typeof r === 'number') {
|
|
@@ -39,8 +103,8 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
39
103
|
return { value: [box(result, typeInfo)], context };
|
|
40
104
|
}
|
|
41
105
|
|
|
42
|
-
// For
|
|
43
|
-
return { value: [
|
|
106
|
+
// For incompatible types, return empty per FHIRPath spec
|
|
107
|
+
return { value: [], context };
|
|
44
108
|
};
|
|
45
109
|
|
|
46
110
|
export const plusOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// power() takes exactly one argument (exponent)
|
|
@@ -75,14 +75,39 @@ export const powerFunction: FunctionDefinition & { evaluate: FunctionEvaluator }
|
|
|
75
75
|
'2.5.power(2)',
|
|
76
76
|
'(-1).power(0.5)'
|
|
77
77
|
],
|
|
78
|
-
signatures: [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
signatures: [
|
|
79
|
+
{
|
|
80
|
+
name: 'power-integer',
|
|
81
|
+
input: { type: 'Integer', singleton: true },
|
|
82
|
+
parameters: [
|
|
83
|
+
{ name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
|
|
84
|
+
],
|
|
85
|
+
result: { type: 'Integer', singleton: true }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'power-decimal',
|
|
89
|
+
input: { type: 'Decimal', singleton: true },
|
|
90
|
+
parameters: [
|
|
91
|
+
{ name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
|
|
92
|
+
],
|
|
93
|
+
result: { type: 'Decimal', singleton: true }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'power-integer-decimal',
|
|
97
|
+
input: { type: 'Integer', singleton: true },
|
|
98
|
+
parameters: [
|
|
99
|
+
{ name: 'exponent', type: { type: 'Decimal', singleton: true }, optional: false }
|
|
100
|
+
],
|
|
101
|
+
result: { type: 'Decimal', singleton: true }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'power-decimal-integer',
|
|
105
|
+
input: { type: 'Decimal', singleton: true },
|
|
106
|
+
parameters: [
|
|
107
|
+
{ name: 'exponent', type: { type: 'Integer', singleton: true }, optional: false }
|
|
108
|
+
],
|
|
109
|
+
result: { type: 'Decimal', singleton: true }
|
|
110
|
+
}
|
|
111
|
+
],
|
|
87
112
|
evaluate
|
|
88
113
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
+
import { Errors } from '../errors';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import { getDecimalPlaces } from '../utils/decimal';
|
|
5
|
+
import { isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
|
|
6
|
+
|
|
7
|
+
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
8
|
+
// Check single item in input
|
|
9
|
+
if (input.length === 0) {
|
|
10
|
+
return { value: [], context };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (input.length > 1) {
|
|
14
|
+
throw Errors.singletonRequired('precision', input.length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const boxedInputValue = input[0];
|
|
18
|
+
if (!boxedInputValue) {
|
|
19
|
+
return { value: [], context };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const inputValue = unbox(boxedInputValue);
|
|
23
|
+
|
|
24
|
+
// precision() takes no arguments
|
|
25
|
+
if (args.length !== 0) {
|
|
26
|
+
throw Errors.wrongArgumentCount('precision', 0, args.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle temporal types - return precision according to FHIRPath spec
|
|
30
|
+
if (isFHIRDate(inputValue)) {
|
|
31
|
+
const date = inputValue as any;
|
|
32
|
+
// Date precision: year=4, year-month=6, year-month-day=8
|
|
33
|
+
if (date.day !== undefined) {
|
|
34
|
+
return { value: [box(8, { type: 'Integer', singleton: true })], context };
|
|
35
|
+
} else if (date.month !== undefined) {
|
|
36
|
+
return { value: [box(6, { type: 'Integer', singleton: true })], context };
|
|
37
|
+
} else {
|
|
38
|
+
return { value: [box(4, { type: 'Integer', singleton: true })], context };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isFHIRDateTime(inputValue)) {
|
|
43
|
+
const dateTime = inputValue as any;
|
|
44
|
+
// DateTime precision: year=4, month=6, day=8, hour=10, minute=12, second=14, millisecond=17
|
|
45
|
+
if (dateTime.millisecond !== undefined) {
|
|
46
|
+
return { value: [box(17, { type: 'Integer', singleton: true })], context };
|
|
47
|
+
} else if (dateTime.second !== undefined) {
|
|
48
|
+
return { value: [box(14, { type: 'Integer', singleton: true })], context };
|
|
49
|
+
} else if (dateTime.minute !== undefined) {
|
|
50
|
+
return { value: [box(12, { type: 'Integer', singleton: true })], context };
|
|
51
|
+
} else if (dateTime.hour !== undefined) {
|
|
52
|
+
return { value: [box(10, { type: 'Integer', singleton: true })], context };
|
|
53
|
+
} else if (dateTime.day !== undefined) {
|
|
54
|
+
return { value: [box(8, { type: 'Integer', singleton: true })], context };
|
|
55
|
+
} else if (dateTime.month !== undefined) {
|
|
56
|
+
return { value: [box(6, { type: 'Integer', singleton: true })], context };
|
|
57
|
+
} else {
|
|
58
|
+
return { value: [box(4, { type: 'Integer', singleton: true })], context };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isFHIRTime(inputValue)) {
|
|
63
|
+
const time = inputValue as any;
|
|
64
|
+
// Time precision: hour=2, minute=4, second=6, millisecond=9
|
|
65
|
+
// But spec shows hour=4, minute=4, second=6, millisecond=9
|
|
66
|
+
// Looking at examples: @T10:30 has "10:30" which is 4 characters for HH:mm
|
|
67
|
+
// @T10:30:00.000 has milliseconds which would be 9 for HH:mm:ss.fff
|
|
68
|
+
if (time.millisecond !== undefined) {
|
|
69
|
+
return { value: [box(9, { type: 'Integer', singleton: true })], context };
|
|
70
|
+
} else if (time.second !== undefined) {
|
|
71
|
+
return { value: [box(6, { type: 'Integer', singleton: true })], context };
|
|
72
|
+
} else if (time.minute !== undefined) {
|
|
73
|
+
return { value: [box(4, { type: 'Integer', singleton: true })], context };
|
|
74
|
+
} else {
|
|
75
|
+
// Hour only would be 2 (HH)
|
|
76
|
+
return { value: [box(2, { type: 'Integer', singleton: true })], context };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle decimal/numeric types
|
|
81
|
+
if (typeof inputValue === 'number') {
|
|
82
|
+
// For integers, precision is always 0
|
|
83
|
+
if (Number.isInteger(inputValue)) {
|
|
84
|
+
return { value: [box(0, { type: 'Integer', singleton: true })], context };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// For decimals, count significant digits after the decimal point
|
|
88
|
+
const decimalPlaces = getDecimalPlaces(inputValue);
|
|
89
|
+
return { value: [box(decimalPlaces, { type: 'Integer', singleton: true })], context };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// For any other type, return empty
|
|
93
|
+
return { value: [], context };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const precisionFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
97
|
+
name: 'precision',
|
|
98
|
+
category: ['utility'],
|
|
99
|
+
description: 'Returns the number of digits of precision for decimal values, or the precision indicator for temporal values (year=4, month=6, day=8, etc.). Returns empty for other types.',
|
|
100
|
+
examples: [
|
|
101
|
+
"1.58700.precision()",
|
|
102
|
+
"@2014.precision()",
|
|
103
|
+
"@2014-01-05T10:30:00.000.precision()",
|
|
104
|
+
"@T10:30.precision()",
|
|
105
|
+
"{}.precision()"
|
|
106
|
+
],
|
|
107
|
+
signatures: [
|
|
108
|
+
{
|
|
109
|
+
name: 'precision',
|
|
110
|
+
input: { type: 'Decimal', singleton: true },
|
|
111
|
+
parameters: [],
|
|
112
|
+
result: { type: 'Integer', singleton: true }
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'precision',
|
|
116
|
+
input: { type: 'Integer', singleton: true },
|
|
117
|
+
parameters: [],
|
|
118
|
+
result: { type: 'Integer', singleton: true }
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'precision',
|
|
122
|
+
input: { type: 'Date', singleton: true },
|
|
123
|
+
parameters: [],
|
|
124
|
+
result: { type: 'Integer', singleton: true }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'precision',
|
|
128
|
+
input: { type: 'DateTime', singleton: true },
|
|
129
|
+
parameters: [],
|
|
130
|
+
result: { type: 'Integer', singleton: true }
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'precision',
|
|
134
|
+
input: { type: 'Time', singleton: true },
|
|
135
|
+
parameters: [],
|
|
136
|
+
result: { type: 'Integer', singleton: true }
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'precision',
|
|
140
|
+
input: { type: 'Any', singleton: true },
|
|
141
|
+
parameters: [],
|
|
142
|
+
result: { type: 'Integer', singleton: false }
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
evaluate
|
|
146
|
+
};
|