@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
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
// Check single item in input
|
|
7
|
+
if (input.length === 0) {
|
|
8
|
+
return { value: [], context };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (input.length > 1) {
|
|
12
|
+
throw Errors.stringSingletonRequired('matches', input.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const boxedInputValue = input[0];
|
|
16
|
+
if (!boxedInputValue) {
|
|
17
|
+
return { value: [], context };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const inputValue = unbox(boxedInputValue);
|
|
21
|
+
if (typeof inputValue !== 'string') {
|
|
22
|
+
throw Errors.stringOperationOnNonString('matches');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check arguments
|
|
26
|
+
if (args.length !== 1) {
|
|
27
|
+
throw Errors.wrongArgumentCount('matches', 1, args.length);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Evaluate regex argument
|
|
31
|
+
const regexArg = args[0];
|
|
32
|
+
if (!regexArg) {
|
|
33
|
+
throw Errors.argumentRequired('matches', 'regex argument');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const regexResult = await evaluator(regexArg, input, context);
|
|
37
|
+
|
|
38
|
+
if (regexResult.value.length === 0) {
|
|
39
|
+
return { value: [], context };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (regexResult.value.length > 1) {
|
|
43
|
+
throw Errors.singletonRequired('matches regex', regexResult.value.length);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const boxedRegex = regexResult.value[0];
|
|
47
|
+
if (!boxedRegex) {
|
|
48
|
+
return { value: [], context };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const regexPattern = unbox(boxedRegex);
|
|
52
|
+
if (typeof regexPattern !== 'string') {
|
|
53
|
+
throw Errors.invalidStringOperation('matches', 'regex argument');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Create regex with unicode support and single line mode (dotAll)
|
|
58
|
+
// Per spec: case-sensitive, single line mode, allow Unicode
|
|
59
|
+
const regex = new RegExp(regexPattern, 'us');
|
|
60
|
+
const result = regex.test(inputValue);
|
|
61
|
+
|
|
62
|
+
return { value: [box(result, { type: 'Boolean', singleton: true })], context };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Invalid regular expression in matches(): ${(error as Error).message}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const matchesFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
69
|
+
name: 'matches',
|
|
70
|
+
category: ['string'],
|
|
71
|
+
description: 'Returns true when the input string matches the given regular expression',
|
|
72
|
+
examples: [
|
|
73
|
+
"'test string'.matches('t.+')",
|
|
74
|
+
"'test string'.matches('asd.+')",
|
|
75
|
+
"'first line\\nsecond line'.matches('line.second')"
|
|
76
|
+
],
|
|
77
|
+
signatures: [{
|
|
78
|
+
name: 'matches',
|
|
79
|
+
input: { type: 'String', singleton: true },
|
|
80
|
+
parameters: [
|
|
81
|
+
{ name: 'regex', type: { type: 'String', singleton: true } }
|
|
82
|
+
],
|
|
83
|
+
result: { type: 'Boolean', singleton: true }
|
|
84
|
+
}],
|
|
85
|
+
evaluate
|
|
86
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
// Check single item in input
|
|
7
|
+
if (input.length === 0) {
|
|
8
|
+
return { value: [], context };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (input.length > 1) {
|
|
12
|
+
throw Errors.stringSingletonRequired('matchesFull', input.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const boxedInputValue = input[0];
|
|
16
|
+
if (!boxedInputValue) {
|
|
17
|
+
return { value: [], context };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const inputValue = unbox(boxedInputValue);
|
|
21
|
+
if (typeof inputValue !== 'string') {
|
|
22
|
+
throw Errors.stringOperationOnNonString('matchesFull');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check arguments
|
|
26
|
+
if (args.length !== 1) {
|
|
27
|
+
throw Errors.wrongArgumentCount('matchesFull', 1, args.length);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Evaluate regex argument
|
|
31
|
+
const regexArg = args[0];
|
|
32
|
+
if (!regexArg) {
|
|
33
|
+
throw Errors.argumentRequired('matchesFull', 'regex argument');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const regexResult = await evaluator(regexArg, input, context);
|
|
37
|
+
|
|
38
|
+
if (regexResult.value.length === 0) {
|
|
39
|
+
return { value: [], context };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (regexResult.value.length > 1) {
|
|
43
|
+
throw Errors.singletonRequired('matchesFull regex', regexResult.value.length);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const boxedRegex = regexResult.value[0];
|
|
47
|
+
if (!boxedRegex) {
|
|
48
|
+
return { value: [], context };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const regexPattern = unbox(boxedRegex);
|
|
52
|
+
if (typeof regexPattern !== 'string') {
|
|
53
|
+
throw Errors.invalidStringOperation('matchesFull', 'regex argument');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// matchesFull implicitly adds ^ and $ anchors to match the entire string
|
|
58
|
+
// Only add anchors if they're not already present
|
|
59
|
+
let fullPattern = regexPattern;
|
|
60
|
+
if (!regexPattern.startsWith('^')) {
|
|
61
|
+
fullPattern = '^' + fullPattern;
|
|
62
|
+
}
|
|
63
|
+
if (!regexPattern.endsWith('$')) {
|
|
64
|
+
fullPattern = fullPattern + '$';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create regex with unicode support and single line mode (dotAll)
|
|
68
|
+
// Per spec: case-sensitive, single line mode, allow Unicode
|
|
69
|
+
const regex = new RegExp(fullPattern, 'us');
|
|
70
|
+
const result = regex.test(inputValue);
|
|
71
|
+
|
|
72
|
+
return { value: [box(result, { type: 'Boolean', singleton: true })], context };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(`Invalid regular expression in matchesFull(): ${(error as Error).message}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const matchesFullFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
79
|
+
name: 'matchesFull',
|
|
80
|
+
category: ['string'],
|
|
81
|
+
description: 'Returns true when the input string completely matches the given regular expression',
|
|
82
|
+
examples: [
|
|
83
|
+
"'Library'.matchesFull('Library')",
|
|
84
|
+
"'http://example.org/Library'.matchesFull('Library')",
|
|
85
|
+
"'N8000123'.matchesFull('N[0-9]{7}')"
|
|
86
|
+
],
|
|
87
|
+
signatures: [{
|
|
88
|
+
name: 'matchesFull',
|
|
89
|
+
input: { type: 'String', singleton: true },
|
|
90
|
+
parameters: [
|
|
91
|
+
{ name: 'regex', type: { type: 'String', singleton: true } }
|
|
92
|
+
],
|
|
93
|
+
result: { type: 'Boolean', singleton: true }
|
|
94
|
+
}],
|
|
95
|
+
evaluate
|
|
96
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// millisecondOf() function - Extracts millisecond 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 millisecondOfEvaluator: FunctionEvaluator = async (input, context, args) => {
|
|
8
|
+
// millisecondOf() takes no arguments
|
|
9
|
+
if (args.length !== 0) {
|
|
10
|
+
throw Errors.wrongArgumentCount('millisecondOf', 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('millisecondOf', 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 millisecond component is present
|
|
33
|
+
if (value.millisecond === undefined) {
|
|
34
|
+
return { value: [], context };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the millisecond as an Integer (0-999)
|
|
38
|
+
return {
|
|
39
|
+
value: [box(value.millisecond, { 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 millisecondOfFunction: FunctionDefinition & { evaluate: typeof millisecondOfEvaluator } = {
|
|
49
|
+
name: 'millisecondOf',
|
|
50
|
+
category: ['temporal'],
|
|
51
|
+
description: 'Returns the millisecond component of a Time or DateTime value (0-999)',
|
|
52
|
+
examples: [
|
|
53
|
+
'@T10:30:45.123.millisecondOf()',
|
|
54
|
+
'@2014-01-05T10:30:45.500.millisecondOf()',
|
|
55
|
+
'Observation.effectiveDateTime.millisecondOf()'
|
|
56
|
+
],
|
|
57
|
+
signatures: [
|
|
58
|
+
{
|
|
59
|
+
name: 'millisecondOf',
|
|
60
|
+
input: { type: 'Any', singleton: true },
|
|
61
|
+
parameters: [],
|
|
62
|
+
result: { type: 'Integer', singleton: true }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
evaluate: millisecondOfEvaluator
|
|
66
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { subtractQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { subtractQuantities } from '../complex-types/quantity-value';
|
|
5
|
+
import type { QuantityValue } from '../complex-types/quantity-value';
|
|
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) {
|
|
@@ -17,6 +18,67 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
17
18
|
if (!boxedr) return { value: [], context };
|
|
18
19
|
const r = unbox(boxedr);
|
|
19
20
|
|
|
21
|
+
// Check for temporal arithmetic using boxed type information
|
|
22
|
+
const leftType = boxedl?.typeInfo?.type;
|
|
23
|
+
const rightType = boxedr?.typeInfo?.type;
|
|
24
|
+
|
|
25
|
+
if ((leftType === 'Date' || leftType === 'DateTime' || leftType === 'Time') && rightType === 'Quantity') {
|
|
26
|
+
// Left is temporal, right is quantity
|
|
27
|
+
const temporalType = leftType;
|
|
28
|
+
const quantity = r as QuantityValue;
|
|
29
|
+
|
|
30
|
+
// Import temporal utilities and create TimeQuantity
|
|
31
|
+
const { createTimeQuantity, subtract } = await import('../complex-types/temporal');
|
|
32
|
+
|
|
33
|
+
// Calendar duration units (allowed for temporal arithmetic)
|
|
34
|
+
const calendarUnits = ['year', 'years', 'month', 'months', 'week', 'weeks',
|
|
35
|
+
'day', 'days', 'hour', 'hours', 'minute', 'minutes',
|
|
36
|
+
'second', 'seconds', 'millisecond', 'milliseconds'];
|
|
37
|
+
|
|
38
|
+
// Variable duration UCUM units (not allowed for temporal arithmetic - they have calendar-dependent durations)
|
|
39
|
+
const variableDurationUnits = ['a', 'mo'];
|
|
40
|
+
|
|
41
|
+
// Fixed duration UCUM units (allowed - they map directly to calendar units)
|
|
42
|
+
const fixedDurationUnitMap: Record<string, string> = {
|
|
43
|
+
'd': 'day',
|
|
44
|
+
'wk': 'week',
|
|
45
|
+
'h': 'hour',
|
|
46
|
+
'min': 'minute',
|
|
47
|
+
's': 'second',
|
|
48
|
+
'ms': 'millisecond'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Check if this is a variable duration unit (not allowed)
|
|
52
|
+
if (variableDurationUnits.includes(quantity.unit)) {
|
|
53
|
+
// Variable duration units like 'a' and 'mo' cannot be subtracted from temporal values
|
|
54
|
+
// because they don't have fixed durations
|
|
55
|
+
const { Errors } = await import('../errors');
|
|
56
|
+
throw Errors.invalidTemporalUnit(temporalType, quantity.unit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Map fixed duration UCUM units to calendar units
|
|
60
|
+
let mappedUnit = fixedDurationUnitMap[quantity.unit] || quantity.unit;
|
|
61
|
+
|
|
62
|
+
// Check if this is a valid calendar duration unit (after mapping)
|
|
63
|
+
if (!calendarUnits.includes(mappedUnit)) {
|
|
64
|
+
// Non-time units with temporal values return empty per FHIRPath spec
|
|
65
|
+
return { value: [], context };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timeQuantity = createTimeQuantity(quantity.value, mappedUnit as any);
|
|
69
|
+
|
|
70
|
+
// Use the functional subtract operation
|
|
71
|
+
const result = subtract(l as any, timeQuantity);
|
|
72
|
+
|
|
73
|
+
if (temporalType === 'Date') {
|
|
74
|
+
return { value: [box(result, { type: 'Date', singleton: true })], context };
|
|
75
|
+
} else if (temporalType === 'DateTime') {
|
|
76
|
+
return { value: [box(result, { type: 'DateTime', singleton: true })], context };
|
|
77
|
+
} else if (temporalType === 'Time') {
|
|
78
|
+
return { value: [box(result, { type: 'Time', singleton: true })], context };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
20
82
|
// Check if both are quantities
|
|
21
83
|
if (l && typeof l === 'object' && 'unit' in l &&
|
|
22
84
|
r && typeof r === 'object' && 'unit' in r) {
|
|
@@ -26,7 +88,17 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
26
88
|
|
|
27
89
|
// Handle numeric subtraction
|
|
28
90
|
if (typeof l === 'number' && typeof r === 'number') {
|
|
29
|
-
|
|
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
|
+
|
|
98
|
+
const typeInfo = Number.isInteger(result) ?
|
|
99
|
+
{ type: 'Integer' as const, singleton: true } :
|
|
100
|
+
{ type: 'Decimal' as const, singleton: true };
|
|
101
|
+
return { value: [box(result, typeInfo)], context };
|
|
30
102
|
}
|
|
31
103
|
|
|
32
104
|
// For other types, return empty
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// minuteOf() function - Extracts minute 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 minuteOfEvaluator: FunctionEvaluator = async (input, context, args) => {
|
|
8
|
+
// minuteOf() takes no arguments
|
|
9
|
+
if (args.length !== 0) {
|
|
10
|
+
throw Errors.wrongArgumentCount('minuteOf', 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('minuteOf', 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 minute component is present
|
|
33
|
+
if (value.minute === undefined) {
|
|
34
|
+
return { value: [], context };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the minute as an Integer (0-59)
|
|
38
|
+
return {
|
|
39
|
+
value: [box(value.minute, { 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 minuteOfFunction: FunctionDefinition & { evaluate: typeof minuteOfEvaluator } = {
|
|
49
|
+
name: 'minuteOf',
|
|
50
|
+
category: ['temporal'],
|
|
51
|
+
description: 'Returns the minute component of a Time or DateTime value (0-59)',
|
|
52
|
+
examples: [
|
|
53
|
+
'@T10:30:00.minuteOf()',
|
|
54
|
+
'@2014-01-05T10:30:00.minuteOf()',
|
|
55
|
+
'Observation.effectiveDateTime.minuteOf()'
|
|
56
|
+
],
|
|
57
|
+
signatures: [
|
|
58
|
+
{
|
|
59
|
+
name: 'minuteOf',
|
|
60
|
+
input: { type: 'Any', singleton: true },
|
|
61
|
+
parameters: [],
|
|
62
|
+
result: { type: 'Integer', singleton: true }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
evaluate: minuteOfEvaluator
|
|
66
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
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';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// monthOf() function - Extracts month component from Date or DateTime
|
|
2
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import { isFHIRDate, isFHIRDateTime } from '../complex-types/temporal';
|
|
5
|
+
import { Errors } from '../errors';
|
|
6
|
+
|
|
7
|
+
export const monthOfEvaluator: FunctionEvaluator = async (input, context, args) => {
|
|
8
|
+
// monthOf() takes no arguments
|
|
9
|
+
if (args.length !== 0) {
|
|
10
|
+
throw Errors.wrongArgumentCount('monthOf', 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('monthOf', 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 Date or DateTime
|
|
31
|
+
if (isFHIRDate(value) || isFHIRDateTime(value)) {
|
|
32
|
+
// Check if month component is present
|
|
33
|
+
if (value.month === undefined) {
|
|
34
|
+
return { value: [], context };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return the month as an Integer (1-12)
|
|
38
|
+
return {
|
|
39
|
+
value: [box(value.month, { type: 'Integer', singleton: true })],
|
|
40
|
+
context
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Not a Date or DateTime, return empty
|
|
45
|
+
return { value: [], context };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const monthOfFunction: FunctionDefinition & { evaluate: typeof monthOfEvaluator } = {
|
|
49
|
+
name: 'monthOf',
|
|
50
|
+
category: ['temporal'],
|
|
51
|
+
description: 'Returns the month component of a Date or DateTime value (1-12)',
|
|
52
|
+
examples: [
|
|
53
|
+
'@2014-01-05.monthOf()',
|
|
54
|
+
'@2014-01-05T10:30:00.monthOf()',
|
|
55
|
+
'Patient.birthDate.monthOf()'
|
|
56
|
+
],
|
|
57
|
+
signatures: [
|
|
58
|
+
{
|
|
59
|
+
name: 'monthOf',
|
|
60
|
+
input: { type: 'Any', singleton: true },
|
|
61
|
+
parameters: [],
|
|
62
|
+
result: { type: 'Integer', singleton: true }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
evaluate: monthOfEvaluator
|
|
66
|
+
};
|
|
@@ -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 { multiplyQuantities } from '../quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../quantity-value';
|
|
6
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { multiplyQuantities } 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) {
|
|
@@ -71,6 +71,30 @@ export const multiplyOperator: OperatorDefinition & { evaluate: OperationEvaluat
|
|
|
71
71
|
left: { type: 'Quantity', singleton: true },
|
|
72
72
|
right: { type: 'Quantity', singleton: true },
|
|
73
73
|
result: { type: 'Quantity', singleton: true },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'quantity-integer-multiply',
|
|
77
|
+
left: { type: 'Quantity', singleton: true },
|
|
78
|
+
right: { type: 'Integer', singleton: true },
|
|
79
|
+
result: { type: 'Quantity', singleton: true },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'integer-quantity-multiply',
|
|
83
|
+
left: { type: 'Integer', singleton: true },
|
|
84
|
+
right: { type: 'Quantity', singleton: true },
|
|
85
|
+
result: { type: 'Quantity', singleton: true },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'quantity-decimal-multiply',
|
|
89
|
+
left: { type: 'Quantity', singleton: true },
|
|
90
|
+
right: { type: 'Decimal', singleton: true },
|
|
91
|
+
result: { type: 'Quantity', singleton: true },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'decimal-quantity-multiply',
|
|
95
|
+
left: { type: 'Decimal', singleton: true },
|
|
96
|
+
right: { type: 'Quantity', singleton: true },
|
|
97
|
+
result: { type: 'Quantity', singleton: true },
|
|
74
98
|
}
|
|
75
99
|
],
|
|
76
100
|
evaluate
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
import type { OperatorDefinition } from '../types';
|
|
1
|
+
import type { OperatorDefinition, OperationEvaluator } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box } from '../interpreter/boxing';
|
|
4
|
+
import { collectionsNotEqual } from './comparison';
|
|
6
5
|
|
|
7
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const boxedl = left[0];
|
|
13
|
-
if (!boxedl) return { value: [], context };
|
|
14
|
-
const l = unbox(boxedl);
|
|
15
|
-
const boxedr = right[0];
|
|
16
|
-
if (!boxedr) return { value: [], context };
|
|
17
|
-
const r = unbox(boxedr);
|
|
7
|
+
// Use the unified comparison system
|
|
8
|
+
const result = collectionsNotEqual(left, right);
|
|
18
9
|
|
|
19
|
-
//
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
const comparison = compareQuantities(l as QuantityValue, r as QuantityValue);
|
|
23
|
-
// If quantities are incomparable (different dimensions), return empty
|
|
24
|
-
if (comparison === null) {
|
|
25
|
-
return { value: [], context };
|
|
26
|
-
}
|
|
27
|
-
return { value: [box(comparison !== 0, { type: 'Boolean', singleton: true })], context };
|
|
10
|
+
// null means incomparable (returns empty)
|
|
11
|
+
if (result === null) {
|
|
12
|
+
return { value: [], context };
|
|
28
13
|
}
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
// Return the boolean result
|
|
16
|
+
return { value: [box(result, { type: 'Boolean', singleton: true })], context };
|
|
31
17
|
};
|
|
32
18
|
|
|
33
19
|
export const notEqualOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -38,11 +24,19 @@ export const notEqualOperator: OperatorDefinition & { evaluate: OperationEvaluat
|
|
|
38
24
|
associativity: 'left',
|
|
39
25
|
description: 'The converse of the equals operator, returning true if equal returns false; false if equal returns true; and empty ({ }) if equal returns empty',
|
|
40
26
|
examples: ['name != "John"', 'Patient.gender != "male"', '5 != 3'],
|
|
41
|
-
signatures: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
signatures: [
|
|
28
|
+
{
|
|
29
|
+
name: 'not-equal',
|
|
30
|
+
left: { type: 'Any', singleton: true },
|
|
31
|
+
right: { type: 'Any', singleton: true },
|
|
32
|
+
result: { type: 'Boolean', singleton: true },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'not-equal',
|
|
36
|
+
left: { type: 'Any', singleton: false },
|
|
37
|
+
right: { type: 'Any', singleton: false },
|
|
38
|
+
result: { type: 'Boolean', singleton: true },
|
|
39
|
+
}
|
|
40
|
+
],
|
|
47
41
|
evaluate
|
|
48
42
|
};
|