@atomic-ehr/fhirpath 0.0.2 → 0.0.3

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 (143) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +225 -119
  3. package/dist/index.js +10911 -5600
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -4
  6. package/src/analyzer/augmentor.ts +242 -0
  7. package/src/analyzer/cursor-services.ts +75 -0
  8. package/src/analyzer/scope-manager.ts +57 -0
  9. package/src/analyzer/trivia-indexer.ts +58 -0
  10. package/src/analyzer/type-compat.ts +157 -0
  11. package/src/analyzer/utils.ts +132 -0
  12. package/src/analyzer.ts +921 -1208
  13. package/src/completion-provider.ts +209 -191
  14. package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
  15. package/src/complex-types/temporal.ts +1737 -0
  16. package/src/errors.ts +25 -3
  17. package/src/index.ts +17 -104
  18. package/src/inspect.ts +4 -4
  19. package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
  20. package/src/interpreter/navigator.ts +94 -0
  21. package/src/interpreter/runtime-context.ts +273 -0
  22. package/src/interpreter.ts +435 -469
  23. package/src/lexer.ts +188 -210
  24. package/src/model-provider.ts +71 -43
  25. package/src/operations/abs-function.ts +1 -1
  26. package/src/operations/aggregate-function.ts +84 -5
  27. package/src/operations/all-function.ts +4 -3
  28. package/src/operations/allFalse-function.ts +2 -1
  29. package/src/operations/allTrue-function.ts +2 -1
  30. package/src/operations/and-operator.ts +2 -1
  31. package/src/operations/anyFalse-function.ts +2 -1
  32. package/src/operations/anyTrue-function.ts +2 -1
  33. package/src/operations/as-function.ts +58 -0
  34. package/src/operations/as-operator.ts +57 -19
  35. package/src/operations/ceiling-function.ts +1 -1
  36. package/src/operations/children-function.ts +14 -5
  37. package/src/operations/combine-function.ts +6 -3
  38. package/src/operations/combine-operator.ts +6 -7
  39. package/src/operations/comparison.ts +692 -0
  40. package/src/operations/contains-function.ts +1 -1
  41. package/src/operations/contains-operator.ts +2 -1
  42. package/src/operations/convertsToBoolean-function.ts +78 -0
  43. package/src/operations/convertsToDecimal-function.ts +82 -0
  44. package/src/operations/convertsToInteger-function.ts +71 -0
  45. package/src/operations/convertsToLong-function.ts +89 -0
  46. package/src/operations/convertsToQuantity-function.ts +116 -0
  47. package/src/operations/convertsToString-function.ts +88 -0
  48. package/src/operations/count-function.ts +2 -1
  49. package/src/operations/dateOf-function.ts +69 -0
  50. package/src/operations/dayOf-function.ts +66 -0
  51. package/src/operations/decimal-boundaries.ts +133 -0
  52. package/src/operations/defineVariable-function.ts +130 -17
  53. package/src/operations/distinct-function.ts +1 -1
  54. package/src/operations/div-operator.ts +1 -1
  55. package/src/operations/divide-operator.ts +12 -7
  56. package/src/operations/dot-operator.ts +1 -1
  57. package/src/operations/empty-function.ts +30 -21
  58. package/src/operations/endsWith-function.ts +6 -1
  59. package/src/operations/equal-operator.ts +23 -32
  60. package/src/operations/equivalent-operator.ts +13 -53
  61. package/src/operations/exclude-function.ts +2 -1
  62. package/src/operations/exists-function.ts +4 -3
  63. package/src/operations/first-function.ts +1 -1
  64. package/src/operations/floor-function.ts +1 -1
  65. package/src/operations/greater-operator.ts +20 -3
  66. package/src/operations/greater-or-equal-operator.ts +20 -3
  67. package/src/operations/highBoundary-function.ts +120 -0
  68. package/src/operations/hourOf-function.ts +66 -0
  69. package/src/operations/iif-function.ts +186 -7
  70. package/src/operations/implies-operator.ts +1 -1
  71. package/src/operations/in-operator.ts +2 -1
  72. package/src/operations/index.ts +41 -0
  73. package/src/operations/indexOf-function.ts +1 -1
  74. package/src/operations/intersect-function.ts +1 -1
  75. package/src/operations/is-function.ts +59 -0
  76. package/src/operations/is-operator.ts +20 -9
  77. package/src/operations/isDistinct-function.ts +2 -1
  78. package/src/operations/join-function.ts +1 -1
  79. package/src/operations/last-function.ts +1 -1
  80. package/src/operations/lastIndexOf-function.ts +85 -0
  81. package/src/operations/length-function.ts +1 -1
  82. package/src/operations/less-operator.ts +20 -3
  83. package/src/operations/less-or-equal-operator.ts +20 -3
  84. package/src/operations/less-than.ts +2 -2
  85. package/src/operations/lowBoundary-function.ts +120 -0
  86. package/src/operations/lower-function.ts +1 -1
  87. package/src/operations/matches-function.ts +86 -0
  88. package/src/operations/matchesFull-function.ts +96 -0
  89. package/src/operations/millisecondOf-function.ts +66 -0
  90. package/src/operations/minus-operator.ts +69 -4
  91. package/src/operations/minuteOf-function.ts +66 -0
  92. package/src/operations/mod-operator.ts +1 -1
  93. package/src/operations/monthOf-function.ts +66 -0
  94. package/src/operations/multiply-operator.ts +27 -3
  95. package/src/operations/not-equal-operator.ts +24 -30
  96. package/src/operations/not-equivalent-operator.ts +13 -53
  97. package/src/operations/not-function.ts +1 -1
  98. package/src/operations/ofType-function.ts +8 -12
  99. package/src/operations/or-operator.ts +2 -1
  100. package/src/operations/plus-operator.ts +71 -7
  101. package/src/operations/power-function.ts +35 -10
  102. package/src/operations/repeat-function.ts +169 -0
  103. package/src/operations/replace-function.ts +1 -1
  104. package/src/operations/replaceMatches-function.ts +120 -0
  105. package/src/operations/round-function.ts +1 -1
  106. package/src/operations/secondOf-function.ts +66 -0
  107. package/src/operations/select-function.ts +66 -5
  108. package/src/operations/single-function.ts +1 -1
  109. package/src/operations/skip-function.ts +1 -1
  110. package/src/operations/split-function.ts +1 -1
  111. package/src/operations/sqrt-function.ts +15 -8
  112. package/src/operations/startsWith-function.ts +1 -1
  113. package/src/operations/subsetOf-function.ts +6 -2
  114. package/src/operations/substring-function.ts +1 -1
  115. package/src/operations/supersetOf-function.ts +6 -2
  116. package/src/operations/tail-function.ts +1 -1
  117. package/src/operations/take-function.ts +1 -1
  118. package/src/operations/temporal-functions.ts +555 -0
  119. package/src/operations/timeOf-function.ts +67 -0
  120. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  121. package/src/operations/toBoolean-function.ts +27 -8
  122. package/src/operations/toChars-function.ts +56 -0
  123. package/src/operations/toDecimal-function.ts +27 -8
  124. package/src/operations/toInteger-function.ts +15 -3
  125. package/src/operations/toLong-function.ts +98 -0
  126. package/src/operations/toQuantity-function.ts +181 -0
  127. package/src/operations/toString-function.ts +45 -3
  128. package/src/operations/trace-function.ts +1 -1
  129. package/src/operations/trim-function.ts +1 -1
  130. package/src/operations/truncate-function.ts +1 -1
  131. package/src/operations/unary-minus-operator.ts +2 -2
  132. package/src/operations/unary-plus-operator.ts +1 -1
  133. package/src/operations/union-function.ts +1 -1
  134. package/src/operations/union-operator.ts +16 -26
  135. package/src/operations/upper-function.ts +1 -1
  136. package/src/operations/where-function.ts +3 -3
  137. package/src/operations/xor-operator.ts +1 -1
  138. package/src/operations/yearOf-function.ts +66 -0
  139. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  140. package/src/parser.ts +248 -501
  141. package/src/registry.ts +53 -42
  142. package/src/types.ts +128 -16
  143. package/src/utils/pprint.ts +151 -0
@@ -2,7 +2,7 @@ import type { OperatorDefinition } from '../types';
2
2
  import { Errors } from '../errors';
3
3
  import { PRECEDENCE } from '../types';
4
4
  import type { OperationEvaluator } from '../types';
5
- import { box, unbox } from '../boxing';
5
+ import { box, unbox } from '../interpreter/boxing';
6
6
 
7
7
  export const evaluate: OperationEvaluator = async (input, context, left, right) => {
8
8
  // If right is empty, result is empty
@@ -40,6 +40,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
40
40
  export const containsOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
41
41
  symbol: 'contains',
42
42
  name: 'contains',
43
+ doesNotPropagateEmpty: true, // Empty left operand returns false
43
44
  category: ['membership'],
44
45
  precedence: PRECEDENCE.IN_CONTAINS,
45
46
  associativity: 'left',
@@ -0,0 +1,78 @@
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
+ // convertsToBoolean() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('convertsToBoolean', 0, args.length);
9
+ }
10
+
11
+ // If input collection is empty, result is empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input collection contains multiple items, signal an error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('convertsToBoolean', input.length);
19
+ }
20
+
21
+ const boxedInputValue = input[0];
22
+ if (!boxedInputValue) {
23
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
24
+ }
25
+
26
+ const inputValue = unbox(boxedInputValue);
27
+
28
+ // Check if the value can be converted to Boolean
29
+
30
+ // Boolean - always convertible
31
+ if (typeof inputValue === 'boolean') {
32
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
33
+ }
34
+
35
+ // Integer - 1 and 0 are convertible
36
+ if (typeof inputValue === 'number' && Number.isInteger(inputValue)) {
37
+ const canConvert = inputValue === 1 || inputValue === 0;
38
+ return { value: [box(canConvert, { type: 'Boolean', singleton: true })], context };
39
+ }
40
+
41
+ // Decimal - 1.0 and 0.0 are convertible
42
+ if (typeof inputValue === 'number' && !Number.isInteger(inputValue)) {
43
+ const canConvert = inputValue === 1.0 || inputValue === 0.0;
44
+ return { value: [box(canConvert, { type: 'Boolean', singleton: true })], context };
45
+ }
46
+
47
+ // String - check if it's a valid boolean representation (case insensitive)
48
+ if (typeof inputValue === 'string') {
49
+ const lowerValue = inputValue.toLowerCase();
50
+ const validRepresentations = ['true', 't', 'yes', 'y', '1', '1.0', 'false', 'f', 'no', 'n', '0', '0.0'];
51
+ const canConvert = validRepresentations.includes(lowerValue);
52
+ return { value: [box(canConvert, { type: 'Boolean', singleton: true })], context };
53
+ }
54
+
55
+ // For all other types, return false
56
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
57
+ };
58
+
59
+ export const convertsToBooleanFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
60
+ name: 'convertsToBoolean',
61
+ category: ['type-conversion'],
62
+ description: 'Returns true if the input can be converted to a Boolean. Returns true for: Boolean (any), Integer (1 or 0), Decimal (1.0 or 0.0), String (\'true\'/\'t\'/\'yes\'/\'y\'/\'1\'/\'1.0\'/\'false\'/\'f\'/\'no\'/\'n\'/\'0\'/\'0.0\', case insensitive). Returns false for all other values.',
63
+ examples: [
64
+ "'true'.convertsToBoolean()",
65
+ "'invalid'.convertsToBoolean()",
66
+ "1.convertsToBoolean()",
67
+ "2.convertsToBoolean()"
68
+ ],
69
+ signatures: [
70
+ {
71
+ name: 'convertsToBoolean',
72
+ input: { type: 'Any', singleton: true },
73
+ parameters: [],
74
+ result: { type: 'Boolean', singleton: true }
75
+ }
76
+ ],
77
+ evaluate
78
+ };
@@ -0,0 +1,82 @@
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
+ // convertsToDecimal() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('convertsToDecimal', 0, args.length);
9
+ }
10
+
11
+ // If input collection is empty, result is empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input collection contains multiple items, signal an error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('convertsToDecimal', input.length);
19
+ }
20
+
21
+ const boxedInputValue = input[0];
22
+ if (!boxedInputValue) {
23
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
24
+ }
25
+
26
+ const inputValue = unbox(boxedInputValue);
27
+
28
+ // Check if the value can be converted to Decimal
29
+
30
+ // Integer or Decimal - always convertible
31
+ if (typeof inputValue === 'number') {
32
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
33
+ }
34
+
35
+ // Boolean - always convertible (true -> 1.0, false -> 0.0)
36
+ if (typeof inputValue === 'boolean') {
37
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
38
+ }
39
+
40
+ // String - check if valid decimal format
41
+ if (typeof inputValue === 'string') {
42
+ // Use the regex from the spec: (\+|-)?\d+(\.\d+)?
43
+ const decimalRegex = /^(\+|-)?\d+(\.\d+)?$/;
44
+
45
+ if (!decimalRegex.test(inputValue)) {
46
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
47
+ }
48
+
49
+ const parsedValue = parseFloat(inputValue);
50
+
51
+ // Check for valid number
52
+ if (isNaN(parsedValue)) {
53
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
54
+ }
55
+
56
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
57
+ }
58
+
59
+ // For all other types, return false
60
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
61
+ };
62
+
63
+ export const convertsToDecimalFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
64
+ name: 'convertsToDecimal',
65
+ category: ['type-conversion'],
66
+ description: 'Returns true if the input can be converted to a Decimal. Returns true for: Integer (any), Decimal (any), Boolean (any), String matching regex (\\+|-)?\\d+(\\.\\d+)?. Returns false for all other types.',
67
+ examples: [
68
+ "'42'.convertsToDecimal()",
69
+ "'3.14'.convertsToDecimal()",
70
+ "true.convertsToDecimal()",
71
+ "'invalid'.convertsToDecimal()"
72
+ ],
73
+ signatures: [
74
+ {
75
+ name: 'convertsToDecimal',
76
+ input: { type: 'Any', singleton: true },
77
+ parameters: [],
78
+ result: { type: 'Boolean', singleton: true }
79
+ }
80
+ ],
81
+ evaluate
82
+ };
@@ -0,0 +1,71 @@
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
+ // convertsToInteger() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('convertsToInteger', 0, args.length);
9
+ }
10
+
11
+ // If input collection is empty, result is empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input collection contains multiple items, signal an error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('convertsToInteger', input.length);
19
+ }
20
+
21
+ const boxedInputValue = input[0];
22
+ if (!boxedInputValue) {
23
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
24
+ }
25
+
26
+ const inputValue = unbox(boxedInputValue);
27
+
28
+ // Check if the value can be converted to Integer
29
+
30
+ // Integer - always convertible
31
+ if (typeof inputValue === 'number' && Number.isInteger(inputValue)) {
32
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
33
+ }
34
+
35
+ // String - check if valid integer format
36
+ if (typeof inputValue === 'string') {
37
+ // Regex from spec: (\+|-)?\d+
38
+ const integerRegex = /^(\+|-)?\d+$/;
39
+ const canConvert = integerRegex.test(inputValue);
40
+ return { value: [box(canConvert, { type: 'Boolean', singleton: true })], context };
41
+ }
42
+
43
+ // Boolean - always convertible (true -> 1, false -> 0)
44
+ if (typeof inputValue === 'boolean') {
45
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
46
+ }
47
+
48
+ // For all other types (including decimals), return false
49
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
50
+ };
51
+
52
+ export const convertsToIntegerFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
53
+ name: 'convertsToInteger',
54
+ category: ['type-conversion'],
55
+ description: 'Returns true if the input can be converted to an Integer. Returns true for: Integer (any), String matching regex (\\+|-)?\\d+, Boolean (any). Returns false for all other types including decimals.',
56
+ examples: [
57
+ "'42'.convertsToInteger()",
58
+ "'3.14'.convertsToInteger()",
59
+ "true.convertsToInteger()",
60
+ "3.14.convertsToInteger()"
61
+ ],
62
+ signatures: [
63
+ {
64
+ name: 'convertsToInteger',
65
+ input: { type: 'Any', singleton: true },
66
+ parameters: [],
67
+ result: { type: 'Boolean', singleton: true }
68
+ }
69
+ ],
70
+ evaluate
71
+ };
@@ -0,0 +1,89 @@
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
+ // convertsToLong() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('convertsToLong', 0, args.length);
9
+ }
10
+
11
+ // If input collection is empty, result is empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input collection contains multiple items, signal an error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('convertsToLong', input.length);
19
+ }
20
+
21
+ const boxedInputValue = input[0];
22
+ if (!boxedInputValue) {
23
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
24
+ }
25
+
26
+ const inputValue = unbox(boxedInputValue);
27
+
28
+ // Check if the value can be converted to Long
29
+ // Note: In JavaScript, we don't have a separate Long type, but we can check
30
+ // if the value would be a valid long (integer within safe bounds)
31
+
32
+ // Integer - always convertible to Long
33
+ if (typeof inputValue === 'number' && Number.isInteger(inputValue)) {
34
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
35
+ }
36
+
37
+ // String - check if valid integer format (same as convertsToInteger)
38
+ if (typeof inputValue === 'string') {
39
+ // Regex from spec: (\+|-)?\d+
40
+ const integerRegex = /^(\+|-)?\d+$/;
41
+ if (!integerRegex.test(inputValue)) {
42
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
43
+ }
44
+
45
+ // For Long, we should check if the value is within valid bounds
46
+ // In JavaScript, we can safely represent integers up to Number.MAX_SAFE_INTEGER
47
+ try {
48
+ const longValue = BigInt(inputValue);
49
+ // Check if it can be safely represented
50
+ // For FHIRPath, Long is typically 64-bit integer
51
+ const MAX_LONG = BigInt('9223372036854775807');
52
+ const MIN_LONG = BigInt('-9223372036854775808');
53
+
54
+ const canConvert = longValue >= MIN_LONG && longValue <= MAX_LONG;
55
+ return { value: [box(canConvert, { type: 'Boolean', singleton: true })], context };
56
+ } catch {
57
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
58
+ }
59
+ }
60
+
61
+ // Boolean - always convertible (true -> 1, false -> 0)
62
+ if (typeof inputValue === 'boolean') {
63
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
64
+ }
65
+
66
+ // For all other types (including decimals), return false
67
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
68
+ };
69
+
70
+ export const convertsToLongFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
71
+ name: 'convertsToLong',
72
+ category: ['type-conversion'],
73
+ description: 'Returns true if the input can be converted to a Long (64-bit integer). Returns true for: Integer (any), String matching regex (\\+|-)?\\d+ within 64-bit bounds, Boolean (any). Returns false for all other types including decimals.',
74
+ examples: [
75
+ "'42'.convertsToLong()",
76
+ "'9223372036854775807'.convertsToLong()",
77
+ "'9223372036854775808'.convertsToLong()",
78
+ "true.convertsToLong()"
79
+ ],
80
+ signatures: [
81
+ {
82
+ name: 'convertsToLong',
83
+ input: { type: 'Any', singleton: true },
84
+ parameters: [],
85
+ result: { type: 'Boolean', singleton: true }
86
+ }
87
+ ],
88
+ evaluate
89
+ };
@@ -0,0 +1,116 @@
1
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { createQuantity, isValidQuantity } from '../complex-types/quantity-value';
5
+
6
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
7
+ // convertsToQuantity() takes no arguments
8
+ if (args.length !== 0) {
9
+ throw Errors.wrongArgumentCount('convertsToQuantity', 0, args.length);
10
+ }
11
+
12
+ // If input collection is empty, result is empty
13
+ if (input.length === 0) {
14
+ return { value: [], context };
15
+ }
16
+
17
+ // If input collection contains multiple items, signal an error
18
+ if (input.length > 1) {
19
+ throw Errors.singletonRequired('convertsToQuantity', input.length);
20
+ }
21
+
22
+ const boxedInputValue = input[0];
23
+ if (!boxedInputValue) {
24
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
25
+ }
26
+
27
+ const inputValue = unbox(boxedInputValue);
28
+ const typeInfo = boxedInputValue.typeInfo;
29
+
30
+ // Check if the value can be converted to Quantity
31
+
32
+ // Already a Quantity
33
+ if (typeInfo?.type === 'Quantity') {
34
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
35
+ }
36
+
37
+ // Check for Quantity object structure
38
+ if (inputValue && typeof inputValue === 'object') {
39
+ const obj = inputValue as any;
40
+ // Check if it looks like a Quantity (has value and unit properties)
41
+ if (typeof obj.value === 'number' && typeof obj.unit === 'string') {
42
+ // Validate that the unit is valid (either UCUM or calendar duration)
43
+ try {
44
+ const quantity = createQuantity(obj.value, obj.unit);
45
+ const isValid = isValidQuantity(quantity);
46
+ return { value: [box(isValid, { type: 'Boolean', singleton: true })], context };
47
+ } catch {
48
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
49
+ }
50
+ }
51
+ }
52
+
53
+ // String - check if it can be parsed as a quantity (e.g., "10 mg", "5.5 km")
54
+ if (typeof inputValue === 'string') {
55
+ // Try to parse as quantity: number followed by space(s) and unit
56
+ // This matches the pattern: <number> <unit>
57
+ const quantityRegex = /^(\+|-)?\d+(\.\d+)?\s+.+$/;
58
+
59
+ if (!quantityRegex.test(inputValue.trim())) {
60
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
61
+ }
62
+
63
+ // Split into value and unit parts
64
+ const parts = inputValue.trim().split(/\s+/);
65
+ if (parts.length < 2) {
66
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
67
+ }
68
+
69
+ const valueStr = parts[0];
70
+ const unit = parts.slice(1).join(' ');
71
+
72
+ const value = parseFloat(valueStr!);
73
+ if (isNaN(value)) {
74
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
75
+ }
76
+
77
+ // Check if the unit is valid
78
+ try {
79
+ const quantity = createQuantity(value, unit);
80
+ const isValid = isValidQuantity(quantity);
81
+ return { value: [box(isValid, { type: 'Boolean', singleton: true })], context };
82
+ } catch {
83
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
84
+ }
85
+ }
86
+
87
+ // Integer or Decimal with no unit - not a valid quantity
88
+ // (quantities must have units)
89
+ if (typeof inputValue === 'number') {
90
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
91
+ }
92
+
93
+ // For all other types, return false
94
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
95
+ };
96
+
97
+ export const convertsToQuantityFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
98
+ name: 'convertsToQuantity',
99
+ category: ['type-conversion'],
100
+ description: 'Returns true if the input can be converted to a Quantity. Returns true for: Quantity (any), String in format "number unit" with valid UCUM or calendar duration unit. Returns false for numbers without units and all other types.',
101
+ examples: [
102
+ "'10 mg'.convertsToQuantity()",
103
+ "'5.5 km'.convertsToQuantity()",
104
+ "10.convertsToQuantity()",
105
+ "'invalid'.convertsToQuantity()"
106
+ ],
107
+ signatures: [
108
+ {
109
+ name: 'convertsToQuantity',
110
+ input: { type: 'Any', singleton: true },
111
+ parameters: [],
112
+ result: { type: 'Boolean', singleton: true }
113
+ }
114
+ ],
115
+ evaluate
116
+ };
@@ -0,0 +1,88 @@
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
+ // convertsToString() takes no arguments
7
+ if (args.length !== 0) {
8
+ throw Errors.wrongArgumentCount('convertsToString', 0, args.length);
9
+ }
10
+
11
+ // If input collection is empty, result is empty
12
+ if (input.length === 0) {
13
+ return { value: [], context };
14
+ }
15
+
16
+ // If input collection contains multiple items, signal an error
17
+ if (input.length > 1) {
18
+ throw Errors.singletonRequired('convertsToString', input.length);
19
+ }
20
+
21
+ const boxedInputValue = input[0];
22
+ if (!boxedInputValue) {
23
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
24
+ }
25
+
26
+ const inputValue = unbox(boxedInputValue);
27
+
28
+ // Check if the value can be converted to String
29
+
30
+ // String - always convertible
31
+ if (typeof inputValue === 'string') {
32
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
33
+ }
34
+
35
+ // Integer or Decimal - always convertible
36
+ if (typeof inputValue === 'number') {
37
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
38
+ }
39
+
40
+ // Boolean - always convertible
41
+ if (typeof inputValue === 'boolean') {
42
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
43
+ }
44
+
45
+ // Check for temporal types (Date, Time, DateTime) - these are convertible
46
+ if (inputValue && typeof inputValue === 'object') {
47
+ // Check if it has type property indicating temporal type
48
+ const objWithType = inputValue as any;
49
+ if (objWithType.type === 'Date' || objWithType.type === 'DateTime' || objWithType.type === 'Time') {
50
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
51
+ }
52
+
53
+ // Check boxed type info
54
+ const typeInfo = boxedInputValue.typeInfo;
55
+ if (typeInfo?.type === 'Date' || typeInfo?.type === 'DateTime' || typeInfo?.type === 'Time') {
56
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
57
+ }
58
+
59
+ // Check for Quantity type
60
+ if (objWithType.type === 'Quantity' || typeInfo?.type === 'Quantity') {
61
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
62
+ }
63
+ }
64
+
65
+ // For complex objects and other types, return false
66
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
67
+ };
68
+
69
+ export const convertsToStringFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
70
+ name: 'convertsToString',
71
+ category: ['type-conversion'],
72
+ description: 'Returns true if the input can be converted to a String. Returns true for: String (any), Integer (any), Decimal (any), Boolean (any), Date, DateTime, Time, Quantity. Returns false for complex objects and resources.',
73
+ examples: [
74
+ "'test'.convertsToString()",
75
+ "42.convertsToString()",
76
+ "true.convertsToString()",
77
+ "Patient.convertsToString()"
78
+ ],
79
+ signatures: [
80
+ {
81
+ name: 'convertsToString',
82
+ input: { type: 'Any', singleton: true },
83
+ parameters: [],
84
+ result: { type: 'Boolean', singleton: true }
85
+ }
86
+ ],
87
+ evaluate
88
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition } from '../types';
2
2
  import type { FunctionEvaluator } from '../types';
3
- import { box, unbox } from '../boxing';
3
+ import { box, unbox } from '../interpreter/boxing';
4
4
 
5
5
  export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
6
6
  return {
@@ -11,6 +11,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
11
11
 
12
12
  export const countFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
13
13
  name: 'count',
14
+ doesNotPropagateEmpty: true,
14
15
  category: ['collection'],
15
16
  description: 'Returns the number of items in the collection',
16
17
  examples: ['Patient.name.count()'],
@@ -0,0 +1,69 @@
1
+ // dateOf() function - Extracts date component from Date or DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { createDate, isFHIRDate, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const dateOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // dateOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('dateOf', 0, args.length);
11
+ }
12
+
13
+ // Empty input returns empty
14
+ if (input.length === 0) {
15
+ return { value: [], context };
16
+ }
17
+
18
+ // Multiple items throws error
19
+ if (input.length > 1) {
20
+ throw Errors.singletonRequired('dateOf', input.length);
21
+ }
22
+
23
+ const boxedValue = input[0];
24
+ if (!boxedValue) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const value = unbox(boxedValue);
29
+
30
+ // Check if it's a Date or DateTime
31
+ if (isFHIRDate(value)) {
32
+ // Already a Date, return as-is
33
+ return {
34
+ value: [box(value, { type: 'Date', singleton: true })],
35
+ context
36
+ };
37
+ }
38
+
39
+ if (isFHIRDateTime(value)) {
40
+ // Extract date component (preserve precision)
41
+ const date = createDate(value.year, value.month, value.day);
42
+ return {
43
+ value: [box(date, { type: 'Date', singleton: true })],
44
+ context
45
+ };
46
+ }
47
+
48
+ // Not a Date or DateTime, return empty
49
+ return { value: [], context };
50
+ };
51
+
52
+ export const dateOfFunction: FunctionDefinition & { evaluate: typeof dateOfEvaluator } = {
53
+ name: 'dateOf',
54
+ category: ['temporal'],
55
+ description: 'Returns the date component of a Date or DateTime value',
56
+ examples: [
57
+ '@2012-01-01T12:30:00.dateOf()',
58
+ 'Patient.birthDate.dateOf()'
59
+ ],
60
+ signatures: [
61
+ {
62
+ name: 'dateOf',
63
+ input: { type: 'Any', singleton: true },
64
+ parameters: [],
65
+ result: { type: 'Date', singleton: true }
66
+ }
67
+ ],
68
+ evaluate: dateOfEvaluator
69
+ };