@atomic-ehr/fhirpath 0.0.2 → 0.0.3
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 +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- 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 +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -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 +435 -469
- package/src/lexer.ts +188 -210
- 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 +58 -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 +692 -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 +116 -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/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- 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 +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- 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 +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- 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 +1 -1
- package/src/operations/ofType-function.ts +8 -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/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -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 +45 -3
- 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 +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
|
@@ -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 { collectionsEquivalent } from './comparison';
|
|
5
6
|
|
|
6
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
return { value: [box(true, { type: 'Boolean', singleton: true })], context };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Different sizes are not equivalent
|
|
13
|
-
if (left.length !== right.length) {
|
|
14
|
-
return { value: [box(false, { type: 'Boolean', singleton: true })], context };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// For single items, check type-specific 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 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 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 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 equality for now
|
|
45
|
-
// TODO: Implement full 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 collectionsEquivalent function from comparison.ts
|
|
9
|
+
const result = collectionsEquivalent(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(false, { 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 equivalentOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -67,6 +26,7 @@ export const equivalentOperator: OperatorDefinition & { evaluate: OperationEvalu
|
|
|
67
26
|
category: ['equality'],
|
|
68
27
|
precedence: PRECEDENCE.EQUALITY,
|
|
69
28
|
associativity: 'left',
|
|
29
|
+
doesNotPropagateEmpty: true, // Empty collections are valid operands for equivalence
|
|
70
30
|
description: 'Returns true if the collections are the same. For single items: strings are compared case-insensitive with normalized whitespace, decimals are rounded to least precision, dates with different precision return false. For collections: order-independent comparison. Empty ~ empty returns true (unlike =)',
|
|
71
31
|
examples: [
|
|
72
32
|
"'abc' ~ 'ABC'",
|
|
@@ -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
|
if (args.length !== 1) {
|
|
@@ -44,6 +44,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
44
44
|
|
|
45
45
|
export const excludeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
46
46
|
name: 'exclude',
|
|
47
|
+
doesNotPropagateEmpty: true, // exclude with empty argument returns the original collection
|
|
47
48
|
category: ['collection'],
|
|
48
49
|
description: 'Returns the set of elements that are not in the other collection. Duplicate items will not be eliminated by this function, and order will be preserved.',
|
|
49
50
|
examples: [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FunctionDefinition } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { RuntimeContextManager } from '../interpreter';
|
|
3
|
+
import { RuntimeContextManager } from '../interpreter/runtime-context';
|
|
4
4
|
import { type FunctionEvaluator } from '../types';
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
5
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
6
6
|
|
|
7
7
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
8
8
|
// No arguments - just check if input is not empty
|
|
@@ -42,6 +42,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
42
42
|
|
|
43
43
|
export const existsFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
44
44
|
name: 'exists',
|
|
45
|
+
doesNotPropagateEmpty: true,
|
|
45
46
|
category: ['collection', 'logical'],
|
|
46
47
|
description: 'Returns true if the collection has any items, or if any item satisfies the condition',
|
|
47
48
|
examples: ['Patient.name.exists()', 'Patient.name.exists(use = "official")'],
|
|
@@ -55,4 +56,4 @@ export const existsFunction: FunctionDefinition & { evaluate: FunctionEvaluator
|
|
|
55
56
|
result: { type: 'Boolean', singleton: true },
|
|
56
57
|
}],
|
|
57
58
|
evaluate
|
|
58
|
-
};
|
|
59
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition } from '../types';
|
|
2
2
|
import type { FunctionEvaluator } from '../types';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
if (input.length > 0) {
|
|
@@ -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
|
// floor() takes no arguments
|
|
@@ -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 { compareQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { compareQuantities } 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) {
|
|
@@ -24,6 +24,23 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
24
24
|
return { value: result !== null ? [box(result > 0, { type: 'Boolean', singleton: true })] : [], context };
|
|
25
25
|
}
|
|
26
26
|
|
|
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
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
return { value: [box((l as any) > (r as any), { type: 'Boolean', singleton: true })], context };
|
|
28
45
|
};
|
|
29
46
|
|
|
@@ -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 { compareQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { compareQuantities } 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) {
|
|
@@ -24,6 +24,23 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
24
24
|
return { value: result !== null ? [box(result >= 0, { type: 'Boolean', singleton: true })] : [], context };
|
|
25
25
|
}
|
|
26
26
|
|
|
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
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
return { value: [box((l as any) >= (r as any), { type: 'Boolean', singleton: true })], context };
|
|
28
45
|
};
|
|
29
46
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// highBoundary() function - Returns the greatest possible value to the specified precision
|
|
2
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import {
|
|
5
|
+
isFHIRDate, isFHIRDateTime, isFHIRTime,
|
|
6
|
+
getDateHighBoundary, getDateTimeHighBoundary, getTimeHighBoundary
|
|
7
|
+
} from '../complex-types/temporal';
|
|
8
|
+
import { getDecimalHighBoundary } from './decimal-boundaries';
|
|
9
|
+
import { Errors } from '../errors';
|
|
10
|
+
|
|
11
|
+
export const highBoundaryEvaluator: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
12
|
+
// highBoundary() takes optional precision parameter
|
|
13
|
+
if (args.length > 1) {
|
|
14
|
+
throw Errors.wrongArgumentCountRange('highBoundary', 0, 1, args.length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Empty input returns empty
|
|
18
|
+
if (input.length === 0) {
|
|
19
|
+
return { value: [], context };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Multiple items throws error
|
|
23
|
+
if (input.length > 1) {
|
|
24
|
+
throw Errors.singletonRequired('highBoundary', input.length);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const boxedValue = input[0];
|
|
28
|
+
if (!boxedValue) {
|
|
29
|
+
return { value: [], context };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = unbox(boxedValue);
|
|
33
|
+
|
|
34
|
+
// Get precision if provided
|
|
35
|
+
let precision: number | undefined;
|
|
36
|
+
if (args.length === 1) {
|
|
37
|
+
const precisionResult = await evaluator(args[0]!, input, context);
|
|
38
|
+
const precisionArg = precisionResult.value;
|
|
39
|
+
if (precisionArg.length === 0) {
|
|
40
|
+
return { value: [], context };
|
|
41
|
+
}
|
|
42
|
+
if (precisionArg.length > 1) {
|
|
43
|
+
throw Errors.singletonRequired('highBoundary precision', precisionArg.length);
|
|
44
|
+
}
|
|
45
|
+
const precisionValue = unbox(precisionArg[0]!);
|
|
46
|
+
if (typeof precisionValue !== 'number' || !Number.isInteger(precisionValue)) {
|
|
47
|
+
throw Errors.invalidOperandType('highBoundary precision', typeof precisionValue);
|
|
48
|
+
}
|
|
49
|
+
precision = precisionValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle Date
|
|
53
|
+
if (isFHIRDate(value)) {
|
|
54
|
+
const result = getDateHighBoundary(value, precision);
|
|
55
|
+
if (!result) {
|
|
56
|
+
return { value: [], context };
|
|
57
|
+
}
|
|
58
|
+
return { value: [box(result, { type: 'Date', singleton: true })], context };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle DateTime
|
|
62
|
+
if (isFHIRDateTime(value)) {
|
|
63
|
+
const result = getDateTimeHighBoundary(value, precision);
|
|
64
|
+
if (!result) {
|
|
65
|
+
return { value: [], context };
|
|
66
|
+
}
|
|
67
|
+
return { value: [box(result, { type: 'DateTime', singleton: true })], context };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle Time
|
|
71
|
+
if (isFHIRTime(value)) {
|
|
72
|
+
const result = getTimeHighBoundary(value, precision);
|
|
73
|
+
if (!result) {
|
|
74
|
+
return { value: [], context };
|
|
75
|
+
}
|
|
76
|
+
return { value: [box(result, { type: 'Time', singleton: true })], context };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// For Decimal/Integer types
|
|
80
|
+
if (typeof value === 'number') {
|
|
81
|
+
const result = getDecimalHighBoundary(value, precision);
|
|
82
|
+
if (result === null) {
|
|
83
|
+
return { value: [], context };
|
|
84
|
+
}
|
|
85
|
+
// Determine the result type based on whether it's an integer or decimal
|
|
86
|
+
const isInteger = Number.isInteger(result);
|
|
87
|
+
return {
|
|
88
|
+
value: [box(result, { type: isInteger ? 'Integer' : 'Decimal', singleton: true })],
|
|
89
|
+
context
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Invalid type returns empty
|
|
94
|
+
return { value: [], context };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const highBoundaryFunction: FunctionDefinition & { evaluate: typeof highBoundaryEvaluator } = {
|
|
98
|
+
name: 'highBoundary',
|
|
99
|
+
category: ['utility'],
|
|
100
|
+
description: 'Returns the greatest possible value of the input to the specified precision',
|
|
101
|
+
examples: [
|
|
102
|
+
'@2014.highBoundary(6)',
|
|
103
|
+
'@2014-01-01T08.highBoundary(17)',
|
|
104
|
+
'@T10:30.highBoundary(9)',
|
|
105
|
+
'1.587.highBoundary()',
|
|
106
|
+
'1.587.highBoundary(2)',
|
|
107
|
+
'1.highBoundary(0)'
|
|
108
|
+
],
|
|
109
|
+
signatures: [
|
|
110
|
+
{
|
|
111
|
+
name: 'highBoundary',
|
|
112
|
+
input: { type: 'Any' as const, singleton: true },
|
|
113
|
+
parameters: [
|
|
114
|
+
{ name: 'precision', type: { type: 'Integer' as const, singleton: true }, optional: true }
|
|
115
|
+
],
|
|
116
|
+
result: { type: 'Any' as const, singleton: true }
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
evaluate: highBoundaryEvaluator
|
|
120
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// hourOf() function - Extracts hour component from Time or DateTime
|
|
2
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import { isFHIRTime, isFHIRDateTime } from '../complex-types/temporal';
|
|
5
|
+
import { Errors } from '../errors';
|
|
6
|
+
|
|
7
|
+
export const hourOfEvaluator: FunctionEvaluator = async (input, context, args) => {
|
|
8
|
+
// hourOf() takes no arguments
|
|
9
|
+
if (args.length !== 0) {
|
|
10
|
+
throw Errors.wrongArgumentCount('hourOf', 0, args.length);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Empty input returns empty
|
|
14
|
+
if (input.length === 0) {
|
|
15
|
+
return { value: [], context };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Multiple items throws error
|
|
19
|
+
if (input.length > 1) {
|
|
20
|
+
throw Errors.singletonRequired('hourOf', input.length);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const boxedValue = input[0];
|
|
24
|
+
if (!boxedValue) {
|
|
25
|
+
return { value: [], context };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const value = unbox(boxedValue);
|
|
29
|
+
|
|
30
|
+
// Check if it's a Time or DateTime
|
|
31
|
+
if (isFHIRTime(value) || isFHIRDateTime(value)) {
|
|
32
|
+
// Check if hour component is present
|
|
33
|
+
if (value.hour === undefined) {
|
|
34
|
+
return { value: [], context };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the hour as an Integer (0-23)
|
|
38
|
+
return {
|
|
39
|
+
value: [box(value.hour, { type: 'Integer', singleton: true })],
|
|
40
|
+
context
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Not a Time or DateTime, return empty
|
|
45
|
+
return { value: [], context };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const hourOfFunction: FunctionDefinition & { evaluate: typeof hourOfEvaluator } = {
|
|
49
|
+
name: 'hourOf',
|
|
50
|
+
category: ['temporal'],
|
|
51
|
+
description: 'Returns the hour component of a Time or DateTime value (0-23)',
|
|
52
|
+
examples: [
|
|
53
|
+
'@T10:30:00.hourOf()',
|
|
54
|
+
'@2014-01-05T10:30:00.hourOf()',
|
|
55
|
+
'Patient.birthDate.hourOf()'
|
|
56
|
+
],
|
|
57
|
+
signatures: [
|
|
58
|
+
{
|
|
59
|
+
name: 'hourOf',
|
|
60
|
+
input: { type: 'Any', singleton: true },
|
|
61
|
+
parameters: [],
|
|
62
|
+
result: { type: 'Integer', singleton: true }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
evaluate: hourOfEvaluator
|
|
66
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { FunctionDefinition } from '../types';
|
|
2
|
-
import { Errors } from '../errors';
|
|
1
|
+
import type { FunctionDefinition, AnalysisContext, InternalAnalysisResult } from '../types';
|
|
2
|
+
import { Errors, ErrorCodes } from '../errors';
|
|
3
3
|
import type { FunctionEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
5
|
-
import { RuntimeContextManager } from '../interpreter';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
|
+
import { RuntimeContextManager } from '../interpreter/runtime-context';
|
|
6
|
+
import { NodeType } from '../parser';
|
|
7
|
+
import { DiagnosticSeverity } from '../types';
|
|
6
8
|
|
|
7
9
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
8
10
|
if (args.length < 2) {
|
|
@@ -15,7 +17,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
15
17
|
|
|
16
18
|
// Check for multiple items in input collection
|
|
17
19
|
if (input.length > 1) {
|
|
18
|
-
throw Errors.
|
|
20
|
+
throw Errors.emptyNotAllowed('iif');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// Always evaluate condition
|
|
@@ -72,6 +74,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
72
74
|
|
|
73
75
|
export const iifFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
74
76
|
name: 'iif',
|
|
77
|
+
doesNotPropagateEmpty: true, // iif evaluates even with empty input
|
|
75
78
|
category: ['control'],
|
|
76
79
|
description: 'If-then-else expression (immediate if)',
|
|
77
80
|
examples: ['iif(gender = "male", "Mr.", "Ms.")'],
|
|
@@ -86,5 +89,181 @@ export const iifFunction: FunctionDefinition & { evaluate: FunctionEvaluator } =
|
|
|
86
89
|
],
|
|
87
90
|
result: { type: 'Any', singleton: false },
|
|
88
91
|
}],
|
|
89
|
-
evaluate
|
|
90
|
-
|
|
92
|
+
evaluate,
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Analysis-time behavior for iif with lazy evaluation.
|
|
96
|
+
* Only analyzes reachable branches when condition is a literal.
|
|
97
|
+
*/
|
|
98
|
+
async analyze(context: AnalysisContext, args): Promise<InternalAnalysisResult> {
|
|
99
|
+
const diagnostics: any[] = [];
|
|
100
|
+
|
|
101
|
+
// Validate argument count
|
|
102
|
+
if (args.length < 2) {
|
|
103
|
+
return {
|
|
104
|
+
type: { type: 'Any', singleton: false },
|
|
105
|
+
diagnostics: [{
|
|
106
|
+
message: 'iif requires at least 2 arguments',
|
|
107
|
+
severity: DiagnosticSeverity.Error,
|
|
108
|
+
code: ErrorCodes.WRONG_ARGUMENT_COUNT,
|
|
109
|
+
source: 'fhirpath',
|
|
110
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (args.length > 3) {
|
|
116
|
+
return {
|
|
117
|
+
type: { type: 'Any', singleton: false },
|
|
118
|
+
diagnostics: [{
|
|
119
|
+
message: 'iif takes at most 3 arguments',
|
|
120
|
+
severity: DiagnosticSeverity.Error,
|
|
121
|
+
code: ErrorCodes.WRONG_ARGUMENT_COUNT,
|
|
122
|
+
source: 'fhirpath',
|
|
123
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
|
|
124
|
+
}]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Analyze condition
|
|
129
|
+
const conditionArg = args[0];
|
|
130
|
+
if (!conditionArg) {
|
|
131
|
+
return {
|
|
132
|
+
type: { type: 'Any', singleton: false },
|
|
133
|
+
diagnostics: [{
|
|
134
|
+
message: 'iif requires a condition argument',
|
|
135
|
+
severity: DiagnosticSeverity.Error,
|
|
136
|
+
code: ErrorCodes.WRONG_ARGUMENT_COUNT,
|
|
137
|
+
source: 'fhirpath',
|
|
138
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
|
|
139
|
+
}]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const condResult = await context.analyzeNode(conditionArg);
|
|
143
|
+
diagnostics.push(...condResult.diagnostics);
|
|
144
|
+
|
|
145
|
+
// Check if condition is a literal boolean
|
|
146
|
+
let isLiteralTrue = false;
|
|
147
|
+
let isLiteralFalse = false;
|
|
148
|
+
|
|
149
|
+
if (conditionArg.type === NodeType.Literal) {
|
|
150
|
+
const literalValue = (conditionArg as any).value;
|
|
151
|
+
if (literalValue === true) {
|
|
152
|
+
isLiteralTrue = true;
|
|
153
|
+
} else if (literalValue === false) {
|
|
154
|
+
isLiteralFalse = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let trueBranchType = { type: 'Any', singleton: false } as any;
|
|
159
|
+
let falseBranchType = { type: 'Any', singleton: false } as any;
|
|
160
|
+
|
|
161
|
+
// Lazy evaluation: only analyze reachable branches
|
|
162
|
+
if (isLiteralTrue) {
|
|
163
|
+
// Only true branch is reachable
|
|
164
|
+
const trueBranch = args[1];
|
|
165
|
+
if (!trueBranch) {
|
|
166
|
+
return {
|
|
167
|
+
type: { type: 'Any', singleton: false },
|
|
168
|
+
diagnostics
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const trueBranchResult = await context.analyzeNode(trueBranch);
|
|
172
|
+
diagnostics.push(...trueBranchResult.diagnostics);
|
|
173
|
+
trueBranchType = trueBranchResult.type;
|
|
174
|
+
|
|
175
|
+
// Warn about unreachable false branch
|
|
176
|
+
if (args.length === 3 && args[2]) {
|
|
177
|
+
diagnostics.push({
|
|
178
|
+
message: 'Unreachable code: false branch will never execute',
|
|
179
|
+
severity: DiagnosticSeverity.Warning,
|
|
180
|
+
code: ErrorCodes.UNREACHABLE_CODE,
|
|
181
|
+
source: 'fhirpath',
|
|
182
|
+
range: args[2].range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { type: trueBranchType, diagnostics };
|
|
187
|
+
} else if (isLiteralFalse) {
|
|
188
|
+
// Only false branch is reachable (if it exists)
|
|
189
|
+
// Warn about unreachable true branch
|
|
190
|
+
const trueBranch = args[1];
|
|
191
|
+
if (trueBranch) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
message: 'Unreachable code: true branch will never execute',
|
|
194
|
+
severity: DiagnosticSeverity.Warning,
|
|
195
|
+
code: ErrorCodes.UNREACHABLE_CODE,
|
|
196
|
+
source: 'fhirpath',
|
|
197
|
+
range: trueBranch.range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (args.length === 3 && args[2]) {
|
|
202
|
+
const falseBranchResult = await context.analyzeNode(args[2]);
|
|
203
|
+
diagnostics.push(...falseBranchResult.diagnostics);
|
|
204
|
+
falseBranchType = falseBranchResult.type;
|
|
205
|
+
return { type: falseBranchType, diagnostics };
|
|
206
|
+
} else {
|
|
207
|
+
// No else branch, return empty
|
|
208
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// Dynamic condition: analyze both branches
|
|
212
|
+
const trueBranch = args[1];
|
|
213
|
+
if (trueBranch) {
|
|
214
|
+
const trueBranchResult = await context.analyzeNode(trueBranch);
|
|
215
|
+
diagnostics.push(...trueBranchResult.diagnostics);
|
|
216
|
+
trueBranchType = trueBranchResult.type;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (args.length === 3 && args[2]) {
|
|
220
|
+
const falseBranchResult = await context.analyzeNode(args[2]);
|
|
221
|
+
diagnostics.push(...falseBranchResult.diagnostics);
|
|
222
|
+
falseBranchType = falseBranchResult.type;
|
|
223
|
+
|
|
224
|
+
// Result type is the union of both branches
|
|
225
|
+
// For now, if they differ, return Any
|
|
226
|
+
if (trueBranchType.type === falseBranchType.type &&
|
|
227
|
+
trueBranchType.singleton === falseBranchType.singleton) {
|
|
228
|
+
return { type: trueBranchType, diagnostics };
|
|
229
|
+
} else if (trueBranchType.type === falseBranchType.type) {
|
|
230
|
+
// Same type but different singleton - result is collection
|
|
231
|
+
return { type: { type: trueBranchType.type, singleton: false }, diagnostics };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async inferResultType(analyzer, node, inputType) {
|
|
240
|
+
// iif returns the common type of the true and false branches
|
|
241
|
+
if (node.arguments.length >= 2) {
|
|
242
|
+
const trueBranchType = await (analyzer as any).inferType(node.arguments[1]!, inputType);
|
|
243
|
+
if (node.arguments.length >= 3) {
|
|
244
|
+
const falseBranchType = await (analyzer as any).inferType(node.arguments[2]!, inputType);
|
|
245
|
+
// If both branches have the same type, use that
|
|
246
|
+
if (trueBranchType.type === falseBranchType.type &&
|
|
247
|
+
trueBranchType.singleton === falseBranchType.singleton) {
|
|
248
|
+
return trueBranchType;
|
|
249
|
+
}
|
|
250
|
+
// If types are the same but singleton differs, return as collection
|
|
251
|
+
if (trueBranchType.type === falseBranchType.type) {
|
|
252
|
+
// One is singleton, one is collection - result must be collection
|
|
253
|
+
return { type: trueBranchType.type, singleton: false };
|
|
254
|
+
}
|
|
255
|
+
// Otherwise, check if one is a subtype of the other
|
|
256
|
+
if ((analyzer as any).isTypeCompatible(trueBranchType, falseBranchType)) {
|
|
257
|
+
return falseBranchType;
|
|
258
|
+
}
|
|
259
|
+
if ((analyzer as any).isTypeCompatible(falseBranchType, trueBranchType)) {
|
|
260
|
+
return trueBranchType;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
// Only true branch, result can be that type or empty
|
|
264
|
+
return { ...trueBranchType, singleton: false };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { type: 'Any', singleton: false };
|
|
268
|
+
}
|
|
269
|
+
};
|
|
@@ -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 for implies per spec truth table
|
|
@@ -2,7 +2,7 @@ import type { OperatorDefinition } from '../types';
|
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
3
|
import { PRECEDENCE } from '../types';
|
|
4
4
|
import type { OperationEvaluator } from '../types';
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
5
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
6
6
|
|
|
7
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
8
8
|
// If left is empty, result is empty
|
|
@@ -45,6 +45,7 @@ export const inOperator: OperatorDefinition & { evaluate: OperationEvaluator } =
|
|
|
45
45
|
category: ['membership'],
|
|
46
46
|
precedence: PRECEDENCE.IN_CONTAINS,
|
|
47
47
|
associativity: 'left',
|
|
48
|
+
doesNotPropagateEmpty: true, // Has custom empty handling per spec
|
|
48
49
|
description: 'If the left operand is a collection with a single item, returns true if the item is in the right operand using equality semantics. If the left is empty, the result is empty. If the right is empty, the result is false.',
|
|
49
50
|
examples: ['\'Joe\' in Patient.name.given', '5 in (1 | 2 | 3 | 4 | 5)', 'code in terminologyServer.valueset(\'my-valueset\').code'],
|
|
50
51
|
signatures: [
|