@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.
Files changed (67) hide show
  1. package/dist/index.browser.d.ts +22 -0
  2. package/dist/index.browser.js +15758 -0
  3. package/dist/index.browser.js.map +1 -0
  4. package/dist/index.node.d.ts +24 -0
  5. package/dist/{index.js → index.node.js} +5450 -3809
  6. package/dist/index.node.js.map +1 -0
  7. package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
  8. package/package.json +10 -5
  9. package/src/analyzer.ts +46 -9
  10. package/src/complex-types/quantity-value.ts +131 -9
  11. package/src/complex-types/temporal.ts +45 -6
  12. package/src/errors.ts +4 -0
  13. package/src/index.browser.ts +4 -0
  14. package/src/{index.ts → index.common.ts} +18 -14
  15. package/src/index.node.ts +4 -0
  16. package/src/interpreter/navigator.ts +12 -0
  17. package/src/interpreter/runtime-context.ts +60 -25
  18. package/src/interpreter.ts +118 -33
  19. package/src/lexer.ts +4 -1
  20. package/src/model-provider.browser.ts +35 -0
  21. package/src/{model-provider.ts → model-provider.common.ts} +29 -26
  22. package/src/model-provider.node.ts +41 -0
  23. package/src/operations/allTrue-function.ts +6 -10
  24. package/src/operations/and-operator.ts +2 -2
  25. package/src/operations/as-function.ts +41 -0
  26. package/src/operations/combine-operator.ts +17 -4
  27. package/src/operations/comparison.ts +73 -21
  28. package/src/operations/convertsToQuantity-function.ts +56 -7
  29. package/src/operations/decode-function.ts +114 -0
  30. package/src/operations/divide-operator.ts +3 -3
  31. package/src/operations/encode-function.ts +110 -0
  32. package/src/operations/escape-function.ts +114 -0
  33. package/src/operations/exp-function.ts +65 -0
  34. package/src/operations/extension-function.ts +88 -0
  35. package/src/operations/greater-operator.ts +5 -24
  36. package/src/operations/greater-or-equal-operator.ts +5 -24
  37. package/src/operations/hasValue-function.ts +84 -0
  38. package/src/operations/iif-function.ts +7 -1
  39. package/src/operations/implies-operator.ts +1 -0
  40. package/src/operations/index.ts +11 -0
  41. package/src/operations/is-function.ts +11 -0
  42. package/src/operations/is-operator.ts +187 -5
  43. package/src/operations/less-operator.ts +6 -24
  44. package/src/operations/less-or-equal-operator.ts +5 -24
  45. package/src/operations/less-than.ts +7 -12
  46. package/src/operations/ln-function.ts +62 -0
  47. package/src/operations/log-function.ts +113 -0
  48. package/src/operations/lowBoundary-function.ts +14 -0
  49. package/src/operations/minus-operator.ts +8 -1
  50. package/src/operations/mod-operator.ts +7 -1
  51. package/src/operations/not-function.ts +9 -2
  52. package/src/operations/ofType-function.ts +35 -0
  53. package/src/operations/plus-operator.ts +46 -3
  54. package/src/operations/precision-function.ts +146 -0
  55. package/src/operations/replace-function.ts +19 -19
  56. package/src/operations/replaceMatches-function.ts +5 -0
  57. package/src/operations/sort-function.ts +209 -0
  58. package/src/operations/take-function.ts +1 -1
  59. package/src/operations/toQuantity-function.ts +0 -1
  60. package/src/operations/toString-function.ts +76 -12
  61. package/src/operations/trace-function.ts +20 -3
  62. package/src/operations/unescape-function.ts +119 -0
  63. package/src/operations/where-function.ts +3 -1
  64. package/src/parser.ts +14 -2
  65. package/src/types.ts +7 -5
  66. package/src/utils/decimal.ts +76 -0
  67. 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
- console.log('[FHIRPath trace] (unnamed):', JSON.stringify(input));
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
- console.log(`[FHIRPath trace] ${name}:`, JSON.stringify(projectionResult.value));
54
+ if (shouldTrace()) {
55
+ console.log(`[FHIRPath trace] ${name}:`, JSON.stringify(projectionResult.value));
56
+ }
42
57
  } else {
43
58
  // Otherwise log the input
44
- console.log(`[FHIRPath trace] ${name}:`, JSON.stringify(input));
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
+ '&amp;': '&',
7
+ '&lt;': '<',
8
+ '&gt;': '>',
9
+ '&quot;': '"',
10
+ '&#39;': "'",
11
+ '&#x27;': "'",
12
+ '&#x2F;': '/',
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
+ "'&quot;test&quot;'.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 (passing boxed item)
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
- return this.createLiteralNode(numberValue, 'number', token);
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
- return content.replace(/\\(.)/g, (_, char) => {
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: import('./interpreter/boxing').FHIRPathValue[];
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
+ }