@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
@@ -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
+ '&': '&amp;',
7
+ '<': '&lt;',
8
+ '>': '&gt;',
9
+ '"': '&quot;',
10
+ "'": '&#39;',
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
- // Check if both are quantities
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
- // Check if both are temporal values (Date, DateTime, Time)
28
- if (l && typeof l === 'object' && 'kind' in l &&
29
- r && typeof r === 'object' && 'kind' in r) {
30
- const temporalL = l as any;
31
- const temporalR = r as any;
32
- const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
- if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
- const { compare } = await import('../complex-types/temporal');
35
- const result = compare(temporalL, temporalR);
36
- // null means incomparable (different precisions), returns empty
37
- if (result === null) {
38
- return { value: [], context };
39
- }
40
- return { value: [box(result > 0, { type: 'Boolean', singleton: true })], context };
41
- }
21
+ if (comparisonResult.kind === 'incomparable') {
22
+ return { value: [], context };
42
23
  }
43
24
 
44
- return { value: [box((l as any) > (r as any), { type: 'Boolean', singleton: true })], context };
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
- // Check if both are quantities
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
- // Check if both are temporal values (Date, DateTime, Time)
28
- if (l && typeof l === 'object' && 'kind' in l &&
29
- r && typeof r === 'object' && 'kind' in r) {
30
- const temporalL = l as any;
31
- const temporalR = r as any;
32
- const kinds = ['FHIRDate', 'FHIRDateTime', 'FHIRTime'];
33
- if (kinds.includes(temporalL.kind) && kinds.includes(temporalR.kind)) {
34
- const { compare } = await import('../complex-types/temporal');
35
- const result = compare(temporalL, temporalR);
36
- // null means incomparable (different precisions), returns empty
37
- if (result === null) {
38
- return { value: [], context };
39
- }
40
- return { value: [box(result >= 0, { type: 'Boolean', singleton: true })], context };
41
- }
21
+ if (comparisonResult.kind === 'incomparable') {
22
+ return { value: [], context };
42
23
  }
43
24
 
44
- return { value: [box((l as any) >= (r as any), { type: 'Boolean', singleton: true })], context };
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 criteria returns empty
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()',
@@ -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}`);