@atomic-ehr/fhirpath 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.d.ts +22 -0
- package/dist/index.browser.js +15758 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.node.d.ts +24 -0
- package/dist/{index.js → index.node.js} +5450 -3809
- package/dist/index.node.js.map +1 -0
- package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
- package/package.json +10 -5
- package/src/analyzer.ts +46 -9
- package/src/complex-types/quantity-value.ts +131 -9
- package/src/complex-types/temporal.ts +45 -6
- package/src/errors.ts +4 -0
- package/src/index.browser.ts +4 -0
- package/src/{index.ts → index.common.ts} +18 -14
- package/src/index.node.ts +4 -0
- package/src/interpreter/navigator.ts +12 -0
- package/src/interpreter/runtime-context.ts +60 -25
- package/src/interpreter.ts +118 -33
- package/src/lexer.ts +4 -1
- package/src/model-provider.browser.ts +35 -0
- package/src/{model-provider.ts → model-provider.common.ts} +29 -26
- package/src/model-provider.node.ts +41 -0
- package/src/operations/allTrue-function.ts +6 -10
- package/src/operations/and-operator.ts +2 -2
- package/src/operations/as-function.ts +41 -0
- package/src/operations/combine-operator.ts +17 -4
- package/src/operations/comparison.ts +73 -21
- package/src/operations/convertsToQuantity-function.ts +56 -7
- package/src/operations/decode-function.ts +114 -0
- package/src/operations/divide-operator.ts +3 -3
- package/src/operations/encode-function.ts +110 -0
- package/src/operations/escape-function.ts +114 -0
- package/src/operations/exp-function.ts +65 -0
- package/src/operations/extension-function.ts +88 -0
- package/src/operations/greater-operator.ts +5 -24
- package/src/operations/greater-or-equal-operator.ts +5 -24
- package/src/operations/hasValue-function.ts +84 -0
- package/src/operations/iif-function.ts +7 -1
- package/src/operations/implies-operator.ts +1 -0
- package/src/operations/index.ts +11 -0
- package/src/operations/is-function.ts +11 -0
- package/src/operations/is-operator.ts +187 -5
- package/src/operations/less-operator.ts +6 -24
- package/src/operations/less-or-equal-operator.ts +5 -24
- package/src/operations/less-than.ts +7 -12
- package/src/operations/ln-function.ts +62 -0
- package/src/operations/log-function.ts +113 -0
- package/src/operations/lowBoundary-function.ts +14 -0
- package/src/operations/minus-operator.ts +8 -1
- package/src/operations/mod-operator.ts +7 -1
- package/src/operations/not-function.ts +9 -2
- package/src/operations/ofType-function.ts +35 -0
- package/src/operations/plus-operator.ts +46 -3
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/replace-function.ts +19 -19
- package/src/operations/replaceMatches-function.ts +5 -0
- package/src/operations/sort-function.ts +209 -0
- package/src/operations/take-function.ts +1 -1
- package/src/operations/toQuantity-function.ts +0 -1
- package/src/operations/toString-function.ts +76 -12
- package/src/operations/trace-function.ts +20 -3
- package/src/operations/unescape-function.ts +119 -0
- package/src/operations/where-function.ts +3 -1
- package/src/parser.ts +14 -2
- package/src/types.ts +7 -5
- package/src/utils/decimal.ts +76 -0
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
// Handle empty input collection
|
|
7
|
+
if (input.length === 0) {
|
|
8
|
+
return { value: [], context };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// If input contains multiple items, signal an error
|
|
12
|
+
if (input.length > 1) {
|
|
13
|
+
throw Errors.invalidOperation('encode can only be applied to a singleton string');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const boxedInputValue = input[0];
|
|
17
|
+
if (!boxedInputValue) {
|
|
18
|
+
return { value: [], context };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const inputValue = unbox(boxedInputValue);
|
|
22
|
+
|
|
23
|
+
// Type check the input - must be a string
|
|
24
|
+
if (typeof inputValue !== 'string') {
|
|
25
|
+
throw Errors.stringOperationOnNonString('encode');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// encode() requires exactly one argument (format)
|
|
29
|
+
if (!args || args.length !== 1) {
|
|
30
|
+
throw Errors.invalidOperation('encode requires exactly one argument (format)');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Evaluate the format parameter
|
|
34
|
+
const formatArg = args[0];
|
|
35
|
+
if (!formatArg) {
|
|
36
|
+
return { value: [], context };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const formatResult = await evaluator(formatArg, input, context);
|
|
40
|
+
if (formatResult.value.length === 0) {
|
|
41
|
+
// If no format is specified, the result is empty
|
|
42
|
+
return { value: [], context };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (formatResult.value.length > 1) {
|
|
46
|
+
throw Errors.invalidOperation('encode format must be a singleton string');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const boxedFormat = formatResult.value[0];
|
|
50
|
+
if (!boxedFormat) {
|
|
51
|
+
return { value: [], context };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const format = unbox(boxedFormat);
|
|
55
|
+
if (typeof format !== 'string') {
|
|
56
|
+
throw Errors.invalidOperation('encode format must be a string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let result: string;
|
|
60
|
+
|
|
61
|
+
switch (format.toLowerCase()) {
|
|
62
|
+
case 'hex': {
|
|
63
|
+
// Encode to hexadecimal (lowercase)
|
|
64
|
+
const bytes = Buffer.from(inputValue, 'utf-8');
|
|
65
|
+
result = bytes.toString('hex');
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'base64': {
|
|
69
|
+
// Standard base64 encoding
|
|
70
|
+
const bytes = Buffer.from(inputValue, 'utf-8');
|
|
71
|
+
result = bytes.toString('base64');
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'urlbase64': {
|
|
75
|
+
// URL-safe base64 encoding (using - and _ instead of + and /)
|
|
76
|
+
const bytes = Buffer.from(inputValue, 'utf-8');
|
|
77
|
+
result = bytes.toString('base64url');
|
|
78
|
+
// base64url doesn't use padding, but FHIRPath spec says output should be padded with =
|
|
79
|
+
// Add padding if necessary
|
|
80
|
+
const paddingLength = (4 - (result.length % 4)) % 4;
|
|
81
|
+
result = result + '='.repeat(paddingLength);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
// Unknown format, return empty
|
|
86
|
+
return { value: [], context };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { value: [box(result, { type: 'String', singleton: true })], context };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const encodeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
93
|
+
name: 'encode',
|
|
94
|
+
category: ['string'],
|
|
95
|
+
description: 'Returns the result of encoding the input string in the given format. Available formats are hex, base64, and urlbase64.',
|
|
96
|
+
examples: [
|
|
97
|
+
"'test'.encode('base64') // returns 'dGVzdA=='",
|
|
98
|
+
"'test'.encode('hex') // returns '74657374'",
|
|
99
|
+
"'subjects?_d'.encode('urlbase64') // returns 'c3ViamVjdHM_X2Q='"
|
|
100
|
+
],
|
|
101
|
+
signatures: [{
|
|
102
|
+
name: 'encode',
|
|
103
|
+
input: { type: 'String', singleton: true },
|
|
104
|
+
parameters: [
|
|
105
|
+
{ name: 'format', type: { type: 'String', singleton: true } }
|
|
106
|
+
],
|
|
107
|
+
result: { type: 'String', singleton: true }
|
|
108
|
+
}],
|
|
109
|
+
evaluate
|
|
110
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
+
import { Errors } from '../errors';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
|
|
5
|
+
const htmlEscapeMap: Record<string, string> = {
|
|
6
|
+
'&': '&',
|
|
7
|
+
'<': '<',
|
|
8
|
+
'>': '>',
|
|
9
|
+
'"': '"',
|
|
10
|
+
"'": ''',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const jsonEscapeMap: Record<string, string> = {
|
|
14
|
+
'"': '\\"',
|
|
15
|
+
'\\': '\\\\',
|
|
16
|
+
'\b': '\\b',
|
|
17
|
+
'\f': '\\f',
|
|
18
|
+
'\n': '\\n',
|
|
19
|
+
'\r': '\\r',
|
|
20
|
+
'\t': '\\t',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function escapeHtml(str: string): string {
|
|
24
|
+
return str.replace(/[&<>"']/g, (char) => htmlEscapeMap[char] || char);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function escapeJson(str: string): string {
|
|
28
|
+
return str.replace(/["\\\/\b\f\n\r\t]/g, (char) => jsonEscapeMap[char] || char);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
32
|
+
// escape() takes exactly one argument (target)
|
|
33
|
+
if (args.length !== 1) {
|
|
34
|
+
throw Errors.wrongArgumentCount('escape', 1, args.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If input is empty, return empty
|
|
38
|
+
if (input.length === 0) {
|
|
39
|
+
return { value: [], context };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If input has multiple items, error
|
|
43
|
+
if (input.length > 1) {
|
|
44
|
+
throw Errors.singletonRequired('escape', input.length);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const boxedValue = input[0];
|
|
48
|
+
if (!boxedValue) {
|
|
49
|
+
return { value: [], context };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const value = unbox(boxedValue);
|
|
53
|
+
|
|
54
|
+
// Value must be a string
|
|
55
|
+
if (typeof value !== 'string') {
|
|
56
|
+
throw Errors.invalidOperandType('escape', `${typeof value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Evaluate target
|
|
60
|
+
const targetResult = await evaluator(args[0]!, input, context);
|
|
61
|
+
if (targetResult.value.length === 0) {
|
|
62
|
+
return { value: [], context };
|
|
63
|
+
}
|
|
64
|
+
if (targetResult.value.length > 1) {
|
|
65
|
+
throw Errors.invalidOperation('escape target must be a single value');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const boxedTarget = targetResult.value[0];
|
|
69
|
+
if (!boxedTarget) {
|
|
70
|
+
return { value: [], context };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const target = unbox(boxedTarget);
|
|
74
|
+
if (typeof target !== 'string') {
|
|
75
|
+
throw Errors.invalidOperation('escape target must be a string');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Escape based on target
|
|
79
|
+
let result: string;
|
|
80
|
+
switch (target.toLowerCase()) {
|
|
81
|
+
case 'html':
|
|
82
|
+
result = escapeHtml(value);
|
|
83
|
+
break;
|
|
84
|
+
case 'json':
|
|
85
|
+
result = escapeJson(value);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
// Unknown target, return empty
|
|
89
|
+
return { value: [], context };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { value: [box(result, { type: 'String', singleton: true })], context };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const escapeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
96
|
+
name: 'escape',
|
|
97
|
+
category: ['string'],
|
|
98
|
+
description: 'Escapes a string for a given target (html or json)',
|
|
99
|
+
examples: [
|
|
100
|
+
"'\"test\"'.escape('html')",
|
|
101
|
+
"'\"test\"'.escape('json')"
|
|
102
|
+
],
|
|
103
|
+
signatures: [
|
|
104
|
+
{
|
|
105
|
+
name: 'escape',
|
|
106
|
+
input: { type: 'String', singleton: true },
|
|
107
|
+
parameters: [
|
|
108
|
+
{ name: 'target', type: { type: 'String', singleton: true }, optional: false }
|
|
109
|
+
],
|
|
110
|
+
result: { type: 'String', singleton: true }
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
evaluate
|
|
114
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
// exp() takes no arguments
|
|
7
|
+
if (args.length !== 0) {
|
|
8
|
+
throw Errors.wrongArgumentCount('exp', 0, args.length);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// If input is empty, return empty
|
|
12
|
+
if (input.length === 0) {
|
|
13
|
+
return { value: [], context };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If input has multiple items, error
|
|
17
|
+
if (input.length > 1) {
|
|
18
|
+
throw Errors.singletonRequired('exp', input.length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const boxedValue = input[0];
|
|
22
|
+
if (!boxedValue) return { value: [], context };
|
|
23
|
+
const value = unbox(boxedValue);
|
|
24
|
+
|
|
25
|
+
// Must be a number
|
|
26
|
+
if (typeof value !== 'number') {
|
|
27
|
+
throw Errors.invalidOperandType('exp', `${typeof value}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Calculate e^value
|
|
31
|
+
const result = Math.exp(value);
|
|
32
|
+
|
|
33
|
+
// Check for overflow (JavaScript returns Infinity for very large exponents)
|
|
34
|
+
if (!isFinite(result)) {
|
|
35
|
+
throw Errors.invalidOperation('exp() result is too large to represent');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { value: [box(result, { type: 'Decimal', singleton: true })], context };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const expFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
42
|
+
name: 'exp',
|
|
43
|
+
category: ['math'],
|
|
44
|
+
description: 'Returns e raised to the power of the input number (e^x) as a Decimal.',
|
|
45
|
+
examples: [
|
|
46
|
+
'0.exp()',
|
|
47
|
+
'1.exp()',
|
|
48
|
+
'(-1).exp()'
|
|
49
|
+
],
|
|
50
|
+
signatures: [
|
|
51
|
+
{
|
|
52
|
+
name: 'exp-integer',
|
|
53
|
+
input: { type: 'Integer', singleton: true },
|
|
54
|
+
parameters: [],
|
|
55
|
+
result: { type: 'Decimal', singleton: true }
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'exp-decimal',
|
|
59
|
+
input: { type: 'Decimal', singleton: true },
|
|
60
|
+
parameters: [],
|
|
61
|
+
result: { type: 'Decimal', singleton: true }
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
evaluate
|
|
65
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator, ASTNode, RuntimeContext, NodeEvaluator } from '../types';
|
|
2
|
+
import type { FHIRPathValue } from '../interpreter/boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
import { Errors } from '../errors';
|
|
5
|
+
|
|
6
|
+
const extensionEvaluator: FunctionEvaluator = async (
|
|
7
|
+
input: FHIRPathValue[],
|
|
8
|
+
context: RuntimeContext,
|
|
9
|
+
args: ASTNode[],
|
|
10
|
+
evaluator: NodeEvaluator
|
|
11
|
+
) => {
|
|
12
|
+
// extension() function takes one argument - the URL to match
|
|
13
|
+
if (args.length !== 1) {
|
|
14
|
+
throw Errors.invalidOperation('extension() requires exactly one argument');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Evaluate the URL argument
|
|
18
|
+
const urlResult = await evaluator(args[0]!, input, context);
|
|
19
|
+
if (urlResult.value.length !== 1) {
|
|
20
|
+
throw Errors.invalidOperation('extension() URL argument must be a single value');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const firstValue = urlResult.value[0];
|
|
24
|
+
if (!firstValue) {
|
|
25
|
+
throw Errors.invalidOperation('extension() URL argument evaluated to null');
|
|
26
|
+
}
|
|
27
|
+
const urlToMatch = unbox(firstValue);
|
|
28
|
+
if (typeof urlToMatch !== 'string') {
|
|
29
|
+
throw Errors.invalidOperation('extension() URL argument must be a string');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results: FHIRPathValue[] = [];
|
|
33
|
+
|
|
34
|
+
// For each input item, look for extensions
|
|
35
|
+
for (const boxedItem of input) {
|
|
36
|
+
const item = unbox(boxedItem);
|
|
37
|
+
|
|
38
|
+
// First check if this is a primitive value with extensions in primitiveElement
|
|
39
|
+
if (boxedItem.primitiveElement?.extension) {
|
|
40
|
+
const extensions = boxedItem.primitiveElement.extension;
|
|
41
|
+
for (const ext of extensions) {
|
|
42
|
+
if (ext && typeof ext === 'object' && 'url' in ext && ext.url === urlToMatch) {
|
|
43
|
+
const boxedExt = box(ext, { type: 'Extension' as any, singleton: true });
|
|
44
|
+
results.push(boxedExt);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Then check for direct extension property on complex types
|
|
50
|
+
if (item && typeof item === 'object' && 'extension' in item) {
|
|
51
|
+
const extensions = (item as any).extension;
|
|
52
|
+
if (Array.isArray(extensions)) {
|
|
53
|
+
for (const ext of extensions) {
|
|
54
|
+
if (ext && typeof ext === 'object' && 'url' in ext && ext.url === urlToMatch) {
|
|
55
|
+
const boxedExt = box(ext, { type: 'Extension' as any, singleton: true });
|
|
56
|
+
results.push(boxedExt);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { value: results, context };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const extensionFunction: FunctionDefinition & { evaluate: typeof extensionEvaluator } = {
|
|
67
|
+
name: 'extension',
|
|
68
|
+
category: ['navigation'],
|
|
69
|
+
description: 'Returns extensions matching the specified URL',
|
|
70
|
+
examples: [
|
|
71
|
+
"Patient.birthDate.extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime')",
|
|
72
|
+
"Observation.extension('http://example.com/fhir/StructureDefinition/patient-age')"
|
|
73
|
+
],
|
|
74
|
+
signatures: [
|
|
75
|
+
{
|
|
76
|
+
name: 'extension-by-url',
|
|
77
|
+
parameters: [{
|
|
78
|
+
name: 'url',
|
|
79
|
+
type: { type: 'String', singleton: true },
|
|
80
|
+
expression: true
|
|
81
|
+
}],
|
|
82
|
+
input: { type: 'Any', singleton: false },
|
|
83
|
+
result: { type: 'Extension' as any, singleton: false }
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
doesNotPropagateEmpty: false,
|
|
87
|
+
evaluate: extensionEvaluator
|
|
88
|
+
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { compareQuantities } from '../complex-types/quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../complex-types/quantity-value';
|
|
6
4
|
import { box, unbox } from '../interpreter/boxing';
|
|
5
|
+
import { compare } from './comparison';
|
|
7
6
|
|
|
8
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
9
8
|
if (left.length === 0 || right.length === 0) {
|
|
@@ -17,31 +16,13 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
17
16
|
if (!boxedr) return { value: [], context };
|
|
18
17
|
const r = unbox(boxedr);
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
if (l && typeof l === 'object' && 'unit' in l &&
|
|
22
|
-
r && typeof r === 'object' && 'unit' in r) {
|
|
23
|
-
const result = compareQuantities(l as QuantityValue, r as QuantityValue);
|
|
24
|
-
return { value: result !== null ? [box(result > 0, { type: 'Boolean', singleton: true })] : [], context };
|
|
25
|
-
}
|
|
19
|
+
const comparisonResult = compare(l, r);
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
r && typeof r === 'object' && 'kind' in r) {
|
|
30
|
-
const temporalL = l as any;
|
|
31
|
-
const temporalR = r as any;
|
|
32
|
-
const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
|
|
33
|
-
if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
|
|
34
|
-
const { compare } = await import('../complex-types/temporal');
|
|
35
|
-
const result = compare(temporalL, temporalR);
|
|
36
|
-
// null means incomparable (different precisions), returns empty
|
|
37
|
-
if (result === null) {
|
|
38
|
-
return { value: [], context };
|
|
39
|
-
}
|
|
40
|
-
return { value: [box(result > 0, { type: 'Boolean', singleton: true })], context };
|
|
41
|
-
}
|
|
21
|
+
if (comparisonResult.kind === 'incomparable') {
|
|
22
|
+
return { value: [], context };
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
return { value: [box(
|
|
25
|
+
return { value: [box(comparisonResult.kind === 'greater', { type: 'Boolean', singleton: true })], context };
|
|
45
26
|
};
|
|
46
27
|
|
|
47
28
|
export const greaterOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { compareQuantities } from '../complex-types/quantity-value';
|
|
5
|
-
import type { QuantityValue } from '../complex-types/quantity-value';
|
|
6
4
|
import { box, unbox } from '../interpreter/boxing';
|
|
5
|
+
import { compare } from './comparison';
|
|
7
6
|
|
|
8
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
9
8
|
if (left.length === 0 || right.length === 0) {
|
|
@@ -17,31 +16,13 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
17
16
|
if (!boxedr) return { value: [], context };
|
|
18
17
|
const r = unbox(boxedr);
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
if (l && typeof l === 'object' && 'unit' in l &&
|
|
22
|
-
r && typeof r === 'object' && 'unit' in r) {
|
|
23
|
-
const result = compareQuantities(l as QuantityValue, r as QuantityValue);
|
|
24
|
-
return { value: result !== null ? [box(result >= 0, { type: 'Boolean', singleton: true })] : [], context };
|
|
25
|
-
}
|
|
19
|
+
const comparisonResult = compare(l, r);
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
r && typeof r === 'object' && 'kind' in r) {
|
|
30
|
-
const temporalL = l as any;
|
|
31
|
-
const temporalR = r as any;
|
|
32
|
-
const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
|
|
33
|
-
if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
|
|
34
|
-
const { compare } = await import('../complex-types/temporal');
|
|
35
|
-
const result = compare(temporalL, temporalR);
|
|
36
|
-
// null means incomparable (different precisions), returns empty
|
|
37
|
-
if (result === null) {
|
|
38
|
-
return { value: [], context };
|
|
39
|
-
}
|
|
40
|
-
return { value: [box(result >= 0, { type: 'Boolean', singleton: true })], context };
|
|
41
|
-
}
|
|
21
|
+
if (comparisonResult.kind === 'incomparable') {
|
|
22
|
+
return { value: [], context };
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
return { value: [box(
|
|
25
|
+
return { value: [box(comparisonResult.kind === 'greater' || comparisonResult.kind === 'equal', { type: 'Boolean', singleton: true })], context };
|
|
45
26
|
};
|
|
46
27
|
|
|
47
28
|
export const greaterOrEqualOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
3
|
+
import { isTemporalValue } from '../complex-types/temporal';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* hasValue() : Boolean
|
|
7
|
+
*
|
|
8
|
+
* Returns true if the input collection contains a single value which is a FHIR primitive,
|
|
9
|
+
* and it has a primitive value (e.g. as opposed to not having a value and just having extensions).
|
|
10
|
+
* Otherwise, the return value is empty.
|
|
11
|
+
*
|
|
12
|
+
* In FHIR, primitives can be represented as either:
|
|
13
|
+
* 1. Simple values: "birthDate": "1974-12-25"
|
|
14
|
+
* 2. Complex with extensions: "_birthDate": { "extension": [...] } (no value)
|
|
15
|
+
* 3. Both: "birthDate": "1974-12-25" with "_birthDate": { "extension": [...] }
|
|
16
|
+
*
|
|
17
|
+
* This function returns true only if there's an actual primitive value (cases 1 and 3).
|
|
18
|
+
*/
|
|
19
|
+
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
20
|
+
// Must be a single value
|
|
21
|
+
if (input.length !== 1) {
|
|
22
|
+
return { value: [], context };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const value = unbox(input[0]!);
|
|
26
|
+
|
|
27
|
+
// Check if it's a primitive type with an actual value
|
|
28
|
+
// Primitives in FHIR are: string, boolean, integer, decimal, uri, url, canonical,
|
|
29
|
+
// base64Binary, instant, date, dateTime, time, code, oid, id, markdown, unsignedInt, positiveInt
|
|
30
|
+
// In FHIRPath/JS, these map to: string, boolean, number, Date, DateTime, Time
|
|
31
|
+
|
|
32
|
+
// Check for primitive JavaScript types
|
|
33
|
+
if (value === null || value === undefined) {
|
|
34
|
+
// No value present
|
|
35
|
+
return { value: [], context };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Empty string is considered as having no value in FHIR
|
|
39
|
+
if (value === '') {
|
|
40
|
+
return { value: [], context };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if it's a primitive type
|
|
44
|
+
const isPrimitive =
|
|
45
|
+
typeof value === 'string' ||
|
|
46
|
+
typeof value === 'boolean' ||
|
|
47
|
+
typeof value === 'number' ||
|
|
48
|
+
value instanceof Date ||
|
|
49
|
+
isTemporalValue(value); // FHIRDate, FHIRTime, FHIRDateTime are primitives
|
|
50
|
+
|
|
51
|
+
if (!isPrimitive) {
|
|
52
|
+
// Check if it's a FHIR element with a value property
|
|
53
|
+
// This handles cases where we have an object representation of a primitive
|
|
54
|
+
if (typeof value === 'object' && value !== null) {
|
|
55
|
+
// If it's an object, it might be a FHIR element with extensions
|
|
56
|
+
// In this case, we need to check if it has an actual value
|
|
57
|
+
// For now, we'll treat any object as not having a primitive value
|
|
58
|
+
// unless it's a recognized type like Date or temporal values
|
|
59
|
+
return { value: [], context };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// It's a primitive with a value
|
|
64
|
+
return { value: [box(true, { type: 'Boolean', singleton: true })], context };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const hasValueFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
68
|
+
name: 'hasValue',
|
|
69
|
+
doesNotPropagateEmpty: true,
|
|
70
|
+
category: ['existence'],
|
|
71
|
+
description: 'Returns true if the input collection contains a single value which is a FHIR primitive, and it has a primitive value (e.g. as opposed to not having a value and just having extensions). Otherwise, the return value is empty.',
|
|
72
|
+
examples: [
|
|
73
|
+
'Patient.birthDate.hasValue()',
|
|
74
|
+
'Patient.active.hasValue()',
|
|
75
|
+
'Observation.valueQuantity.value.hasValue()'
|
|
76
|
+
],
|
|
77
|
+
signatures: [{
|
|
78
|
+
name: 'hasValue',
|
|
79
|
+
input: { type: 'Any', singleton: false },
|
|
80
|
+
parameters: [],
|
|
81
|
+
result: { type: 'Boolean', singleton: true }
|
|
82
|
+
}],
|
|
83
|
+
evaluate
|
|
84
|
+
};
|
|
@@ -37,6 +37,12 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
37
37
|
|
|
38
38
|
const condResult = await evaluator(condExpr, input, evalContext);
|
|
39
39
|
|
|
40
|
+
// Check if condition is a singleton boolean
|
|
41
|
+
if (condResult.value.length > 1) {
|
|
42
|
+
// Multiple items in condition - should error per spec
|
|
43
|
+
throw Errors.invalidOperation('iif condition must be a single boolean value');
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
// Empty condition is treated as false
|
|
41
47
|
if (condResult.value.length === 0) {
|
|
42
48
|
// If no else expression provided, return empty
|
|
@@ -56,7 +62,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
56
62
|
|
|
57
63
|
// Check if condition is a boolean
|
|
58
64
|
if (typeof condition !== 'boolean') {
|
|
59
|
-
// Non-boolean
|
|
65
|
+
// Non-boolean singleton returns empty per FHIRPath spec behavior
|
|
60
66
|
return { value: [], context };
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -50,6 +50,7 @@ export const impliesOperator: OperatorDefinition & { evaluate: OperationEvaluato
|
|
|
50
50
|
category: ['logical'],
|
|
51
51
|
precedence: PRECEDENCE.IMPLIES,
|
|
52
52
|
associativity: 'right',
|
|
53
|
+
doesNotPropagateEmpty: true, // Implies has special empty handling rules
|
|
53
54
|
description: 'If the left operand evaluates to true, returns the boolean evaluation of the right operand. If the left operand evaluates to false, returns true. Otherwise, returns true if the right operand evaluates to true, and empty ({ }) otherwise',
|
|
54
55
|
examples: [
|
|
55
56
|
'Patient.name.given.exists() implies Patient.name.family.exists()',
|
package/src/operations/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ export { firstFunction } from './first-function';
|
|
|
50
50
|
export { lastFunction } from './last-function';
|
|
51
51
|
export { childrenFunction } from './children-function';
|
|
52
52
|
export { descendantsFunction } from './descendants-function';
|
|
53
|
+
export { extensionFunction } from './extension-function';
|
|
53
54
|
export { skipFunction } from './skip-function';
|
|
54
55
|
export { takeFunction } from './take-function';
|
|
55
56
|
export { tailFunction } from './tail-function';
|
|
@@ -58,9 +59,11 @@ export { countFunction } from './count-function';
|
|
|
58
59
|
export { existsFunction } from './exists-function';
|
|
59
60
|
export { allFunction } from './all-function';
|
|
60
61
|
export { emptyFunction } from './empty-function';
|
|
62
|
+
export { hasValueFunction } from './hasValue-function';
|
|
61
63
|
export { notFunction } from './not-function';
|
|
62
64
|
export { distinctFunction } from './distinct-function';
|
|
63
65
|
export { isDistinctFunction } from './isDistinct-function';
|
|
66
|
+
export { sortFunction } from './sort-function';
|
|
64
67
|
export { iifFunction } from './iif-function';
|
|
65
68
|
export { defineVariableFunction } from './defineVariable-function';
|
|
66
69
|
|
|
@@ -98,6 +101,10 @@ export { allTrueFunction } from './allTrue-function';
|
|
|
98
101
|
export { splitFunction } from './split-function';
|
|
99
102
|
export { upperFunction } from './upper-function';
|
|
100
103
|
export { lowerFunction } from './lower-function';
|
|
104
|
+
export { escapeFunction } from './escape-function';
|
|
105
|
+
export { unescapeFunction } from './unescape-function';
|
|
106
|
+
export { encodeFunction } from './encode-function';
|
|
107
|
+
export { decodeFunction } from './decode-function';
|
|
101
108
|
export { allFalseFunction } from './allFalse-function';
|
|
102
109
|
export { anyTrueFunction } from './anyTrue-function';
|
|
103
110
|
export { anyFalseFunction } from './anyFalse-function';
|
|
@@ -120,6 +127,7 @@ export { convertsToLongFunction } from './convertsToLong-function';
|
|
|
120
127
|
|
|
121
128
|
// Utility functions
|
|
122
129
|
export { traceFunction } from './trace-function';
|
|
130
|
+
export { precisionFunction } from './precision-function';
|
|
123
131
|
export { dateOfFunction } from './dateOf-function';
|
|
124
132
|
export { timeOfFunction } from './timeOf-function';
|
|
125
133
|
export { yearOfFunction } from './yearOf-function';
|
|
@@ -144,3 +152,6 @@ export { roundFunction } from './round-function';
|
|
|
144
152
|
export { truncateFunction } from './truncate-function';
|
|
145
153
|
export { sqrtFunction } from './sqrt-function';
|
|
146
154
|
export { powerFunction } from './power-function';
|
|
155
|
+
export { logFunction } from './log-function';
|
|
156
|
+
export { lnFunction } from './ln-function';
|
|
157
|
+
export { expFunction } from './exp-function';
|
|
@@ -25,6 +25,17 @@ const isEvaluator: FunctionEvaluator = async (
|
|
|
25
25
|
|
|
26
26
|
if (isIdentifierNode(typeArg)) {
|
|
27
27
|
typeName = typeArg.name;
|
|
28
|
+
} else if (typeArg.type === NodeType.Binary && typeArg.operator === '.') {
|
|
29
|
+
// Handle namespaced types like System.Boolean or FHIR.Patient
|
|
30
|
+
// Reconstruct the full type name from the binary expression
|
|
31
|
+
const leftPart = typeArg.left;
|
|
32
|
+
const rightPart = typeArg.right;
|
|
33
|
+
|
|
34
|
+
if (isIdentifierNode(leftPart) && isIdentifierNode(rightPart)) {
|
|
35
|
+
typeName = `${leftPart.name}.${rightPart.name}`;
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error(`is() requires a type name as argument, got complex expression`);
|
|
38
|
+
}
|
|
28
39
|
} else {
|
|
29
40
|
// For other node types, try to get the name
|
|
30
41
|
throw new Error(`is() requires a type name as argument, got ${typeArg.type}`);
|