@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
|
@@ -2,11 +2,24 @@ import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
3
|
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
|
+
// Check if trace output should be suppressed (during tests or when explicitly disabled)
|
|
6
|
+
const shouldTrace = () => {
|
|
7
|
+
// Suppress trace output during tests
|
|
8
|
+
if (process.env.NODE_ENV === 'test') return false;
|
|
9
|
+
// Allow explicit disabling via FHIRPATH_TRACE=false
|
|
10
|
+
if (process.env.FHIRPATH_TRACE === 'false') return false;
|
|
11
|
+
// Check if we're running under bun test
|
|
12
|
+
if (typeof Bun !== 'undefined' && process.argv.some(arg => arg.includes('bun:test'))) return false;
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
|
|
5
16
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
17
|
// trace() requires at least a name argument
|
|
7
18
|
if (args.length === 0) {
|
|
8
19
|
// If no name provided, use a default name
|
|
9
|
-
|
|
20
|
+
if (shouldTrace()) {
|
|
21
|
+
console.log('[FHIRPath trace] (unnamed):', JSON.stringify(input));
|
|
22
|
+
}
|
|
10
23
|
return { value: input, context };
|
|
11
24
|
}
|
|
12
25
|
|
|
@@ -38,10 +51,14 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
38
51
|
// If projection argument is provided, evaluate it and log the result
|
|
39
52
|
if (args.length === 2 && args[1]) {
|
|
40
53
|
const projectionResult = await evaluator(args[1], input, context);
|
|
41
|
-
|
|
54
|
+
if (shouldTrace()) {
|
|
55
|
+
console.log(`[FHIRPath trace] ${name}:`, JSON.stringify(projectionResult.value));
|
|
56
|
+
}
|
|
42
57
|
} else {
|
|
43
58
|
// Otherwise log the input
|
|
44
|
-
|
|
59
|
+
if (shouldTrace()) {
|
|
60
|
+
console.log(`[FHIRPath trace] ${name}:`, JSON.stringify(input));
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
// Always return the input unchanged
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
+
import { Errors } from '../errors';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
|
+
|
|
5
|
+
const htmlUnescapeMap: Record<string, string> = {
|
|
6
|
+
'&': '&',
|
|
7
|
+
'<': '<',
|
|
8
|
+
'>': '>',
|
|
9
|
+
'"': '"',
|
|
10
|
+
''': "'",
|
|
11
|
+
''': "'",
|
|
12
|
+
'/': '/',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function unescapeHtml(str: string): string {
|
|
16
|
+
return str.replace(/&(?:amp|lt|gt|quot|#39|#x27|#x2F);/g, (entity) => htmlUnescapeMap[entity] || entity);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unescapeJson(str: string): string {
|
|
20
|
+
// Handle JSON escape sequences
|
|
21
|
+
return str.replace(/\\(.)/g, (match, char) => {
|
|
22
|
+
switch (char) {
|
|
23
|
+
case '"': return '"';
|
|
24
|
+
case '\\': return '\\';
|
|
25
|
+
case '/': return '/';
|
|
26
|
+
case 'b': return '\b';
|
|
27
|
+
case 'f': return '\f';
|
|
28
|
+
case 'n': return '\n';
|
|
29
|
+
case 'r': return '\r';
|
|
30
|
+
case 't': return '\t';
|
|
31
|
+
default: return match; // Unknown escape, leave as is
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
37
|
+
// unescape() takes exactly one argument (target)
|
|
38
|
+
if (args.length !== 1) {
|
|
39
|
+
throw Errors.wrongArgumentCount('unescape', 1, args.length);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If input is empty, return empty
|
|
43
|
+
if (input.length === 0) {
|
|
44
|
+
return { value: [], context };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If input has multiple items, error
|
|
48
|
+
if (input.length > 1) {
|
|
49
|
+
throw Errors.singletonRequired('unescape', input.length);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const boxedValue = input[0];
|
|
53
|
+
if (!boxedValue) {
|
|
54
|
+
return { value: [], context };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const value = unbox(boxedValue);
|
|
58
|
+
|
|
59
|
+
// Value must be a string
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
throw Errors.invalidOperandType('unescape', `${typeof value}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Evaluate target
|
|
65
|
+
const targetResult = await evaluator(args[0]!, input, context);
|
|
66
|
+
if (targetResult.value.length === 0) {
|
|
67
|
+
return { value: [], context };
|
|
68
|
+
}
|
|
69
|
+
if (targetResult.value.length > 1) {
|
|
70
|
+
throw Errors.invalidOperation('unescape target must be a single value');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const boxedTarget = targetResult.value[0];
|
|
74
|
+
if (!boxedTarget) {
|
|
75
|
+
return { value: [], context };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const target = unbox(boxedTarget);
|
|
79
|
+
if (typeof target !== 'string') {
|
|
80
|
+
throw Errors.invalidOperation('unescape target must be a string');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Unescape based on target
|
|
84
|
+
let result: string;
|
|
85
|
+
switch (target.toLowerCase()) {
|
|
86
|
+
case 'html':
|
|
87
|
+
result = unescapeHtml(value);
|
|
88
|
+
break;
|
|
89
|
+
case 'json':
|
|
90
|
+
result = unescapeJson(value);
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
// Unknown target, return empty
|
|
94
|
+
return { value: [], context };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { value: [box(result, { type: 'String', singleton: true })], context };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const unescapeFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
101
|
+
name: 'unescape',
|
|
102
|
+
category: ['string'],
|
|
103
|
+
description: 'Unescapes a string for a given target (html or json)',
|
|
104
|
+
examples: [
|
|
105
|
+
"'"test"'.unescape('html')",
|
|
106
|
+
"'\\\"test\\\"'.unescape('json')"
|
|
107
|
+
],
|
|
108
|
+
signatures: [
|
|
109
|
+
{
|
|
110
|
+
name: 'unescape',
|
|
111
|
+
input: { type: 'String', singleton: true },
|
|
112
|
+
parameters: [
|
|
113
|
+
{ name: 'target', type: { type: 'String', singleton: true }, optional: false }
|
|
114
|
+
],
|
|
115
|
+
result: { type: 'String', singleton: true }
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
evaluate
|
|
119
|
+
};
|
|
@@ -28,7 +28,9 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Evaluate condition with temporary context
|
|
31
|
+
// Evaluate condition with temporary context
|
|
32
|
+
// When evaluating the condition, standalone functions should use $this as their implicit receiver
|
|
33
|
+
// We pass [boxedItem] which becomes the implicit context for functions
|
|
32
34
|
const condResult = await evaluator(condition, [boxedItem], tempContext);
|
|
33
35
|
|
|
34
36
|
// Include item if condition is true (unbox the boolean result)
|
package/src/parser.ts
CHANGED
|
@@ -471,7 +471,10 @@ export class Parser {
|
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
// Determine if this is an integer or decimal based on the original token text
|
|
475
|
+
// If the token contains a decimal point, it's a decimal even if the value is a whole number
|
|
476
|
+
const isDecimal = token.value.includes('.');
|
|
477
|
+
return this.createLiteralNode(numberValue, isDecimal ? 'decimal' : 'number', token);
|
|
475
478
|
}
|
|
476
479
|
|
|
477
480
|
if (token.type === TokenType.STRING) {
|
|
@@ -682,7 +685,14 @@ export class Parser {
|
|
|
682
685
|
protected parseStringValue(raw: string): string {
|
|
683
686
|
// Remove quotes and handle escape sequences
|
|
684
687
|
const content = raw.slice(1, -1);
|
|
685
|
-
|
|
688
|
+
|
|
689
|
+
// Handle unicode escapes first (\uXXXX)
|
|
690
|
+
let result = content.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => {
|
|
691
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Then handle other escape sequences
|
|
695
|
+
result = result.replace(/\\(.)/g, (_, char) => {
|
|
686
696
|
switch (char) {
|
|
687
697
|
case 'n': return '\n';
|
|
688
698
|
case 'r': return '\r';
|
|
@@ -696,6 +706,8 @@ export class Parser {
|
|
|
696
706
|
default: return char;
|
|
697
707
|
}
|
|
698
708
|
});
|
|
709
|
+
|
|
710
|
+
return result;
|
|
699
711
|
}
|
|
700
712
|
|
|
701
713
|
protected parseIdentifierValue(raw: string): string {
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FHIRPathValue } from './interpreter/boxing';
|
|
1
2
|
import type { Token, TokenType } from './lexer';
|
|
2
3
|
import type { AnyCursorNode } from './parser/cursor-nodes';
|
|
3
4
|
|
|
@@ -9,13 +10,13 @@ export enum PRECEDENCE {
|
|
|
9
10
|
XOR = 30,
|
|
10
11
|
AND = 40,
|
|
11
12
|
IN_CONTAINS = 50,
|
|
13
|
+
AS_IS = 55, // as, is (moved to lower precedence)
|
|
12
14
|
EQUALITY = 60, // =, !=, ~, !~
|
|
15
|
+
PIPE = 65, // | (moved to lower precedence)
|
|
13
16
|
COMPARISON = 70, // <, >, <=, >=
|
|
14
|
-
PIPE = 80, // |
|
|
15
17
|
ADDITIVE = 90, // +, -
|
|
16
18
|
MULTIPLICATIVE = 100, // *, /, div, mod
|
|
17
19
|
UNARY = 110, // unary +, -, not
|
|
18
|
-
AS_IS = 120, // as, is
|
|
19
20
|
POSTFIX = 130, // []
|
|
20
21
|
DOT = 140, // . (highest)
|
|
21
22
|
}
|
|
@@ -105,6 +106,7 @@ export interface FunctionSignature {
|
|
|
105
106
|
typeReference?: boolean; // When true, this parameter expects a type name (e.g., ofType(Patient))
|
|
106
107
|
}>;
|
|
107
108
|
result: TypeInfo | 'inputType' | 'inputTypeSingleton' | 'parameterType';
|
|
109
|
+
variadic?: boolean; // When true, the function accepts variable number of arguments
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export interface FunctionDefinition {
|
|
@@ -230,7 +232,7 @@ export interface IdentifierNode extends BaseASTNode {
|
|
|
230
232
|
export interface LiteralNode extends BaseASTNode {
|
|
231
233
|
type: NodeType.Literal;
|
|
232
234
|
value: any;
|
|
233
|
-
valueType: 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
|
|
235
|
+
valueType: 'string' | 'number' | 'decimal' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
export interface TemporalLiteralNode extends BaseASTNode {
|
|
@@ -328,8 +330,8 @@ export interface RuntimeContext {
|
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
// Evaluation result - everything is a collection of boxed values
|
|
331
|
-
export interface EvaluationResult {
|
|
332
|
-
value:
|
|
333
|
+
export interface EvaluationResult<T = any> {
|
|
334
|
+
value: T[];
|
|
333
335
|
context: RuntimeContext;
|
|
334
336
|
}
|
|
335
337
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling decimal precision in FHIRPath
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the number of decimal places in a number
|
|
7
|
+
*/
|
|
8
|
+
export function getDecimalPlaces(n: number): number {
|
|
9
|
+
if (Number.isInteger(n)) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Convert to string and count decimal places
|
|
14
|
+
// Use toFixed(10) then strip trailing zeros to get actual precision
|
|
15
|
+
const str = n.toFixed(10).replace(/0+$/, '').replace(/\.$/, '');
|
|
16
|
+
const decimalIndex = str.indexOf('.');
|
|
17
|
+
|
|
18
|
+
if (decimalIndex === -1) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return str.length - decimalIndex - 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Round a number to a specific number of decimal places
|
|
27
|
+
*/
|
|
28
|
+
export function roundToDecimalPlaces(n: number, places: number): number {
|
|
29
|
+
if (places < 0) {
|
|
30
|
+
throw new Error('Decimal places must be non-negative');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const multiplier = Math.pow(10, places);
|
|
34
|
+
return Math.round(n * multiplier) / multiplier;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a decimal result based on the precision of the operands
|
|
39
|
+
* According to FHIRPath spec, arithmetic operations should preserve
|
|
40
|
+
* the appropriate precision based on the input values
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeDecimalResult(result: number, leftOperand: number, rightOperand: number): number {
|
|
43
|
+
// If result is effectively an integer, return it as is
|
|
44
|
+
if (Number.isInteger(result)) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get the precision of both operands
|
|
49
|
+
const leftPlaces = getDecimalPlaces(leftOperand);
|
|
50
|
+
const rightPlaces = getDecimalPlaces(rightOperand);
|
|
51
|
+
|
|
52
|
+
// For addition and subtraction, use the maximum precision
|
|
53
|
+
// For multiplication, sum the precisions
|
|
54
|
+
// For division and modulo, use a reasonable precision (8 decimal places max)
|
|
55
|
+
const maxPrecision = Math.max(leftPlaces, rightPlaces);
|
|
56
|
+
|
|
57
|
+
// Round to the appropriate precision to eliminate floating point artifacts
|
|
58
|
+
return roundToDecimalPlaces(result, Math.min(maxPrecision, 8));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize a decimal for modulo operation
|
|
63
|
+
* Modulo has special precision rules
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeModuloResult(result: number, leftOperand: number, rightOperand: number): number {
|
|
66
|
+
// If result is effectively an integer, return it as is
|
|
67
|
+
if (Number.isInteger(result)) {
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For modulo, use the precision of the divisor (right operand)
|
|
72
|
+
const rightPlaces = getDecimalPlaces(rightOperand);
|
|
73
|
+
|
|
74
|
+
// Round to the appropriate precision
|
|
75
|
+
return roundToDecimalPlaces(result, Math.min(rightPlaces, 8));
|
|
76
|
+
}
|