@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
@@ -0,0 +1,67 @@
1
+ // timeOf() function - Extracts time component from DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { createTime, isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const timeOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // timeOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('timeOf', 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('timeOf', 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 DateTime
31
+ if (isFHIRDateTime(value)) {
32
+ // Check if time component is present
33
+ if (value.hour === undefined) {
34
+ // No time component present
35
+ return { value: [], context };
36
+ }
37
+
38
+ // Extract time component (preserve precision)
39
+ const time = createTime(value.hour, value.minute, value.second, value.millisecond);
40
+ return {
41
+ value: [box(time, { type: 'Time', singleton: true })],
42
+ context
43
+ };
44
+ }
45
+
46
+ // Not a DateTime, return empty
47
+ return { value: [], context };
48
+ };
49
+
50
+ export const timeOfFunction: FunctionDefinition & { evaluate: typeof timeOfEvaluator } = {
51
+ name: 'timeOf',
52
+ category: ['temporal'],
53
+ description: 'Returns the time component of a DateTime value',
54
+ examples: [
55
+ '@2012-01-01T12:30:00.timeOf()',
56
+ 'Observation.effectiveDateTime.timeOf()'
57
+ ],
58
+ signatures: [
59
+ {
60
+ name: 'timeOf',
61
+ input: { type: 'Any', singleton: true },
62
+ parameters: [],
63
+ result: { type: 'Time', singleton: true }
64
+ }
65
+ ],
66
+ evaluate: timeOfEvaluator
67
+ };
@@ -0,0 +1,69 @@
1
+ // timezoneOffsetOf() function - Extracts timezone offset component from DateTime
2
+ import type { FunctionDefinition, FunctionEvaluator } from '../types';
3
+ import { box, unbox } from '../interpreter/boxing';
4
+ import { isFHIRDateTime } from '../complex-types/temporal';
5
+ import { Errors } from '../errors';
6
+
7
+ export const timezoneOffsetOfEvaluator: FunctionEvaluator = async (input, context, args) => {
8
+ // timezoneOffsetOf() takes no arguments
9
+ if (args.length !== 0) {
10
+ throw Errors.wrongArgumentCount('timezoneOffsetOf', 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('timezoneOffsetOf', 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 DateTime with timezone
31
+ if (isFHIRDateTime(value)) {
32
+ // Check if timezone offset is present
33
+ if (value.timezoneOffset === undefined) {
34
+ return { value: [], context };
35
+ }
36
+
37
+ // Return the timezone offset as a Decimal (hours)
38
+ // The timezoneOffset is stored in minutes, convert to decimal hours
39
+ const offsetInHours = value.timezoneOffset / 60;
40
+
41
+ return {
42
+ value: [box(offsetInHours, { type: 'Decimal', singleton: true })],
43
+ context
44
+ };
45
+ }
46
+
47
+ // Not a DateTime, return empty
48
+ return { value: [], context };
49
+ };
50
+
51
+ export const timezoneOffsetOfFunction: FunctionDefinition & { evaluate: typeof timezoneOffsetOfEvaluator } = {
52
+ name: 'timezoneOffsetOf',
53
+ category: ['temporal'],
54
+ description: 'Returns the timezone offset component of a DateTime value as decimal hours',
55
+ examples: [
56
+ '@2012-01-01T12:30:00.000-07:00.timezoneOffsetOf()',
57
+ '@2012-01-01T12:30:00.000+05:30.timezoneOffsetOf()',
58
+ 'Patient.lastUpdated.timezoneOffsetOf()'
59
+ ],
60
+ signatures: [
61
+ {
62
+ name: 'timezoneOffsetOf',
63
+ input: { type: 'Any', singleton: true },
64
+ parameters: [],
65
+ result: { type: 'Decimal', singleton: true }
66
+ }
67
+ ],
68
+ evaluate: timezoneOffsetOfEvaluator
69
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // toBoolean() takes no arguments
@@ -90,12 +90,31 @@ export const toBooleanFunction: FunctionDefinition & { evaluate: FunctionEvaluat
90
90
  "1.0.toBoolean()",
91
91
  "0.0.toBoolean()"
92
92
  ],
93
- signatures: [{
94
-
95
- name: 'toBoolean',
96
- input: { type: 'Any', singleton: true },
97
- parameters: [],
98
- result: { type: 'Boolean', singleton: true }
99
- }],
93
+ signatures: [
94
+ {
95
+ name: 'toBoolean-boolean',
96
+ input: { type: 'Boolean', singleton: true },
97
+ parameters: [],
98
+ result: { type: 'Boolean', singleton: true }
99
+ },
100
+ {
101
+ name: 'toBoolean-integer',
102
+ input: { type: 'Integer', singleton: true },
103
+ parameters: [],
104
+ result: { type: 'Boolean', singleton: true }
105
+ },
106
+ {
107
+ name: 'toBoolean-decimal',
108
+ input: { type: 'Decimal', singleton: true },
109
+ parameters: [],
110
+ result: { type: 'Boolean', singleton: true }
111
+ },
112
+ {
113
+ name: 'toBoolean-string',
114
+ input: { type: 'String', singleton: true },
115
+ parameters: [],
116
+ result: { type: 'Boolean', singleton: true }
117
+ }
118
+ ],
100
119
  evaluate
101
120
  };
@@ -0,0 +1,56 @@
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
+ // Check single item in input
7
+ if (input.length === 0) {
8
+ return { value: [], context };
9
+ }
10
+
11
+ if (input.length > 1) {
12
+ throw Errors.stringSingletonRequired('toChars', input.length);
13
+ }
14
+
15
+ const boxedInputValue = input[0];
16
+ if (!boxedInputValue) {
17
+ return { value: [], context };
18
+ }
19
+
20
+ const inputValue = unbox(boxedInputValue);
21
+ if (typeof inputValue !== 'string') {
22
+ throw Errors.stringOperationOnNonString('toChars');
23
+ }
24
+
25
+ // Check arguments - toChars takes no arguments
26
+ if (args.length !== 0) {
27
+ throw Errors.wrongArgumentCount('toChars', 0, args.length);
28
+ }
29
+
30
+ // Convert string to array of single-character strings
31
+ // Use Array.from to properly handle Unicode characters (including emoji)
32
+ const chars = Array.from(inputValue);
33
+
34
+ // Box each character as a String
35
+ const result = chars.map(char => box(char, { type: 'String', singleton: true }));
36
+
37
+ return { value: result, context };
38
+ };
39
+
40
+ export const toCharsFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
41
+ name: 'toChars',
42
+ category: ['string'],
43
+ description: 'Returns the list of characters in the input string as a collection',
44
+ examples: [
45
+ "'abc'.toChars()",
46
+ "'Hello'.toChars()",
47
+ "''.toChars()"
48
+ ],
49
+ signatures: [{
50
+ name: 'toChars',
51
+ input: { type: 'String', singleton: true },
52
+ parameters: [],
53
+ result: { type: 'String', singleton: false }
54
+ }],
55
+ evaluate
56
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // Handle empty input collection
@@ -71,12 +71,31 @@ export const toDecimalFunction: FunctionDefinition & { evaluate: FunctionEvaluat
71
71
  "true.toDecimal() // returns 1.0",
72
72
  "false.toDecimal() // returns 0.0"
73
73
  ],
74
- signatures: [{
75
-
76
- name: 'toDecimal',
77
- input: { type: 'Any', singleton: true },
78
- parameters: [],
79
- result: { type: 'Decimal', singleton: true }
80
- }],
74
+ signatures: [
75
+ {
76
+ name: 'toDecimal-integer',
77
+ input: { type: 'Integer', singleton: true },
78
+ parameters: [],
79
+ result: { type: 'Decimal', singleton: true }
80
+ },
81
+ {
82
+ name: 'toDecimal-decimal',
83
+ input: { type: 'Decimal', singleton: true },
84
+ parameters: [],
85
+ result: { type: 'Decimal', singleton: true }
86
+ },
87
+ {
88
+ name: 'toDecimal-boolean',
89
+ input: { type: 'Boolean', singleton: true },
90
+ parameters: [],
91
+ result: { type: 'Decimal', singleton: true }
92
+ },
93
+ {
94
+ name: 'toDecimal-string',
95
+ input: { type: 'String', singleton: true },
96
+ parameters: [],
97
+ result: { type: 'Decimal', singleton: true }
98
+ }
99
+ ],
81
100
  evaluate
82
101
  };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // toInteger() takes no arguments
@@ -67,8 +67,20 @@ export const toIntegerFunction: FunctionDefinition & { evaluate: FunctionEvaluat
67
67
  ],
68
68
  signatures: [
69
69
  {
70
- name: 'toInteger',
71
- input: { type: 'Any', singleton: true },
70
+ name: 'toInteger-integer',
71
+ input: { type: 'Integer', singleton: true },
72
+ parameters: [],
73
+ result: { type: 'Integer', singleton: true }
74
+ },
75
+ {
76
+ name: 'toInteger-string',
77
+ input: { type: 'String', singleton: true },
78
+ parameters: [],
79
+ result: { type: 'Integer', singleton: true }
80
+ },
81
+ {
82
+ name: 'toInteger-boolean',
83
+ input: { type: 'Boolean', singleton: true },
72
84
  parameters: [],
73
85
  result: { type: 'Integer', singleton: true }
74
86
  }
@@ -0,0 +1,98 @@
1
+ import type { FunctionDefinition } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { type FunctionEvaluator } from '../types';
4
+ import { unbox, box } from '../interpreter/boxing';
5
+
6
+ // 64-bit Long bounds
7
+ const MAX_LONG = BigInt('9223372036854775807');
8
+ const MIN_LONG = BigInt('-9223372036854775808');
9
+
10
+ // Regex for integer string format
11
+ const INTEGER_REGEX = /^[+-]?\d+$/;
12
+
13
+ export const evaluate: FunctionEvaluator = async (input, context, args) => {
14
+ // toLong takes no arguments
15
+ if (args.length !== 0) {
16
+ throw Errors.wrongArgumentCount('toLong', 0, args.length);
17
+ }
18
+
19
+ // Check singleton requirement
20
+ if (input.length > 1) {
21
+ throw Errors.singletonRequired('toLong', input.length);
22
+ }
23
+
24
+ if (input.length === 0) {
25
+ return { value: [], context };
26
+ }
27
+
28
+ const item = input[0];
29
+ if (!item) {
30
+ return { value: [], context };
31
+ }
32
+
33
+ const unboxedItem = unbox(item);
34
+ const itemType = (item as any).typeInfo?.type || (item as any).type;
35
+
36
+ let resultValue: number | null = null;
37
+
38
+ // Handle different input types
39
+ if (itemType === 'Integer' || itemType === 'Long') {
40
+ // Integer/Long passthrough
41
+ resultValue = unboxedItem as number;
42
+ } else if (itemType === 'Boolean') {
43
+ // true → 1, false → 0
44
+ resultValue = unboxedItem ? 1 : 0;
45
+ } else if (itemType === 'String') {
46
+ // Try to parse the string as an integer
47
+ const str = unboxedItem as string;
48
+
49
+ // Check if string matches integer format
50
+ if (!INTEGER_REGEX.test(str)) {
51
+ return { value: [], context };
52
+ }
53
+
54
+ try {
55
+ // Use BigInt to parse and check bounds
56
+ const bigIntValue = BigInt(str);
57
+
58
+ // Check 64-bit bounds
59
+ if (bigIntValue < MIN_LONG || bigIntValue > MAX_LONG) {
60
+ return { value: [], context };
61
+ }
62
+
63
+ // Convert back to number (safe within 64-bit bounds)
64
+ resultValue = Number(bigIntValue);
65
+ } catch (e) {
66
+ // Parse failed
67
+ return { value: [], context };
68
+ }
69
+ } else {
70
+ // Other types return empty
71
+ return { value: [], context };
72
+ }
73
+
74
+ // Box the result as Long type
75
+ return {
76
+ value: [box(resultValue, { type: 'Long', singleton: true })],
77
+ context
78
+ };
79
+ };
80
+
81
+ export const toLongFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
82
+ name: 'toLong',
83
+ category: ['conversion'],
84
+ description: 'Converts the input to a 64-bit Long integer. Integers are passed through, Strings are parsed if they match integer format and are within 64-bit bounds, Boolean true becomes 1 and false becomes 0. This is a Standard for Trial Use (STU) feature.',
85
+ examples: [
86
+ '1.toLong() // Returns 1',
87
+ '\'123\'.toLong() // Returns 123',
88
+ 'true.toLong() // Returns 1',
89
+ '\'9223372036854775807\'.toLong() // Max 64-bit value'
90
+ ],
91
+ signatures: [{
92
+ name: 'toLong',
93
+ input: { type: 'Any', singleton: true },
94
+ parameters: [],
95
+ result: { type: 'Long', singleton: true }
96
+ }],
97
+ evaluate
98
+ };
@@ -0,0 +1,181 @@
1
+ import type { FunctionDefinition } from '../types';
2
+ import { Errors } from '../errors';
3
+ import { type FunctionEvaluator } from '../types';
4
+ import { unbox, box } from '../interpreter/boxing';
5
+ import { createQuantity, CALENDAR_DURATION_UNITS } from '../complex-types/quantity-value';
6
+ import { ucum } from '@atomic-ehr/ucum';
7
+
8
+ // Regex for parsing quantity strings according to FHIRPath spec
9
+ // Matches: (?'value'(\+|-)?\d+(\.\d+)?)\s*('(?'unit'[^']+)'|(?'time'[a-zA-Z]+))?
10
+ const QUANTITY_REGEX = /^(?<value>[+-]?\d+(?:\.\d+)?)\s*(?:'(?<quotedUnit>[^']+)'|(?<unquotedUnit>[a-zA-Z]+))?$/;
11
+
12
+ // Calendar duration conversion factors (from spec)
13
+ const CALENDAR_CONVERSIONS: Record<string, Record<string, number>> = {
14
+ 'year': { 'month': 12, 'months': 12, 'day': 365, 'days': 365, 'd': 365 },
15
+ 'years': { 'month': 12, 'months': 12, 'day': 365, 'days': 365, 'd': 365 },
16
+ 'month': { 'day': 30, 'days': 30, 'd': 30 },
17
+ 'months': { 'day': 30, 'days': 30, 'd': 30 },
18
+ 'week': { 'day': 7, 'days': 7, 'd': 7 },
19
+ 'weeks': { 'day': 7, 'days': 7, 'd': 7 },
20
+ 'wk': { 'day': 7, 'days': 7, 'd': 7 },
21
+ 'day': { 'hour': 24, 'hours': 24, 'h': 24 },
22
+ 'days': { 'hour': 24, 'hours': 24, 'h': 24 },
23
+ 'd': { 'hour': 24, 'hours': 24, 'h': 24 },
24
+ 'hour': { 'minute': 60, 'minutes': 60, 'min': 60 },
25
+ 'hours': { 'minute': 60, 'minutes': 60, 'min': 60 },
26
+ 'h': { 'minute': 60, 'minutes': 60, 'min': 60 },
27
+ 'minute': { 'second': 60, 'seconds': 60, 's': 60 },
28
+ 'minutes': { 'second': 60, 'seconds': 60, 's': 60 },
29
+ 'min': { 'second': 60, 'seconds': 60, 's': 60 },
30
+ 'second': { 's': 1 },
31
+ 'seconds': { 's': 1 }
32
+ };
33
+
34
+ // Map common time units to their canonical forms
35
+ const UNIT_MAPPING: Record<string, string> = {
36
+ 'year': 'year',
37
+ 'years': 'year',
38
+ 'month': 'month',
39
+ 'months': 'month',
40
+ 'week': 'week',
41
+ 'weeks': 'week',
42
+ 'day': 'd',
43
+ 'days': 'd',
44
+ 'hour': 'h',
45
+ 'hours': 'h',
46
+ 'minute': 'min',
47
+ 'minutes': 'min',
48
+ 'second': 's',
49
+ 'seconds': 's',
50
+ 'millisecond': 'ms',
51
+ 'milliseconds': 'ms'
52
+ };
53
+
54
+ export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
55
+ // toQuantity takes 0 or 1 argument (optional unit)
56
+ if (args.length > 1) {
57
+ throw Errors.wrongArgumentCountRange('toQuantity', 0, 1, args.length);
58
+ }
59
+
60
+ // Check singleton requirement
61
+ if (input.length > 1) {
62
+ throw Errors.singletonRequired('toQuantity', input.length);
63
+ }
64
+
65
+ if (input.length === 0) {
66
+ return { value: [], context };
67
+ }
68
+
69
+ const item = input[0];
70
+ if (!item) {
71
+ return { value: [], context };
72
+ }
73
+
74
+ const unboxedItem = unbox(item);
75
+ const itemType = (item as any).typeInfo?.type || (item as any).type;
76
+
77
+ let resultQuantity: any = null;
78
+
79
+ // Handle different input types
80
+ if (itemType === 'Integer' || itemType === 'Decimal') {
81
+ // Numbers get default unit '1'
82
+ resultQuantity = createQuantity(unboxedItem as number, '1');
83
+ } else if (itemType === 'Quantity') {
84
+ // Already a quantity
85
+ resultQuantity = unboxedItem;
86
+ } else if (itemType === 'Boolean') {
87
+ // true → 1.0 '1', false → 0.0 '1'
88
+ const value = unboxedItem ? 1.0 : 0.0;
89
+ resultQuantity = createQuantity(value, '1');
90
+ } else if (itemType === 'String') {
91
+ // Try to parse the string as a quantity
92
+ const str = unboxedItem as string;
93
+ const match = QUANTITY_REGEX.exec(str);
94
+
95
+ if (match) {
96
+ const value = parseFloat(match.groups?.value || '0');
97
+ const unit = match.groups?.quotedUnit || match.groups?.unquotedUnit || '1';
98
+
99
+ // Map common time units to canonical forms
100
+ const mappedUnit = UNIT_MAPPING[unit] || unit;
101
+ resultQuantity = createQuantity(value, mappedUnit);
102
+ } else {
103
+ // String doesn't match quantity format
104
+ return { value: [], context };
105
+ }
106
+ } else {
107
+ // Other types return empty
108
+ return { value: [], context };
109
+ }
110
+
111
+ // If unit argument is provided, attempt conversion
112
+ if (args.length === 1) {
113
+ const unitArg = args[0];
114
+ if (!unitArg) {
115
+ throw Errors.invalidOperation('toQuantity: unit parameter is required');
116
+ }
117
+
118
+ // Evaluate the argument to get the unit string
119
+ const unitResult = await evaluator(unitArg, input, context);
120
+ if (unitResult.value.length !== 1) {
121
+ throw Errors.invalidOperation('toQuantity: unit parameter must be a single string value');
122
+ }
123
+
124
+ const unitValue = unitResult.value[0];
125
+ const unitValueType = (unitValue as any)?.typeInfo?.type || (unitValue as any)?.type;
126
+ if (!unitValue || unitValueType !== 'String') {
127
+ throw Errors.invalidOperation('toQuantity: unit parameter must be a string');
128
+ }
129
+
130
+ const targetUnit = unbox(unitValue) as string;
131
+ const mappedTargetUnit = UNIT_MAPPING[targetUnit] || targetUnit;
132
+
133
+ // Try calendar duration conversion first
134
+ const conversions = CALENDAR_CONVERSIONS[resultQuantity.unit];
135
+ if (conversions && conversions[mappedTargetUnit]) {
136
+ const factor = conversions[mappedTargetUnit];
137
+ resultQuantity = createQuantity(resultQuantity.value * factor, mappedTargetUnit);
138
+ } else if (resultQuantity.unit === mappedTargetUnit) {
139
+ // Same unit, no conversion needed
140
+ } else if (!CALENDAR_DURATION_UNITS.has(resultQuantity.unit) &&
141
+ !CALENDAR_DURATION_UNITS.has(mappedTargetUnit)) {
142
+ // Try UCUM conversion
143
+ try {
144
+ const convertedValue = ucum.convert(resultQuantity.value, resultQuantity.unit, mappedTargetUnit);
145
+ resultQuantity = createQuantity(convertedValue, mappedTargetUnit);
146
+ } catch (e) {
147
+ // Conversion failed, return empty
148
+ return { value: [], context };
149
+ }
150
+ } else {
151
+ // Can't convert between these units
152
+ return { value: [], context };
153
+ }
154
+ }
155
+
156
+ return {
157
+ value: [box(resultQuantity, { type: 'Quantity', singleton: true })],
158
+ context
159
+ };
160
+ };
161
+
162
+ export const toQuantityFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
163
+ name: 'toQuantity',
164
+ category: ['conversion'],
165
+ description: 'Converts the input to a Quantity value. Integers and Decimals are converted with default unit \'1\'. Strings are parsed for quantity format. Boolean true becomes 1.0 \'1\', false becomes 0.0 \'1\'. With optional unit parameter, attempts conversion to specified unit.',
166
+ examples: [
167
+ '1.toQuantity() // Returns 1 \'1\'',
168
+ '\'4 days\'.toQuantity() // Returns 4 \'d\'',
169
+ '\'1 \\\'wk\\\'\'.toQuantity(\'d\') // Returns 7 \'d\'',
170
+ 'true.toQuantity() // Returns 1.0 \'1\''
171
+ ],
172
+ signatures: [{
173
+ name: 'toQuantity',
174
+ input: { type: 'Any', singleton: true },
175
+ parameters: [
176
+ { name: 'unit', type: { type: 'String', singleton: true }, optional: true }
177
+ ],
178
+ result: { type: 'Quantity', singleton: true }
179
+ }],
180
+ evaluate
181
+ };
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // toString takes no arguments
@@ -80,8 +80,50 @@ export const toStringFunction: FunctionDefinition & { evaluate: FunctionEvaluato
80
80
  ],
81
81
  signatures: [
82
82
  {
83
- name: 'toString',
84
- input: { type: 'Any', singleton: true },
83
+ name: 'toString-string',
84
+ input: { type: 'String', singleton: true },
85
+ parameters: [],
86
+ result: { type: 'String', singleton: true }
87
+ },
88
+ {
89
+ name: 'toString-integer',
90
+ input: { type: 'Integer', singleton: true },
91
+ parameters: [],
92
+ result: { type: 'String', singleton: true }
93
+ },
94
+ {
95
+ name: 'toString-decimal',
96
+ input: { type: 'Decimal', singleton: true },
97
+ parameters: [],
98
+ result: { type: 'String', singleton: true }
99
+ },
100
+ {
101
+ name: 'toString-boolean',
102
+ input: { type: 'Boolean', singleton: true },
103
+ parameters: [],
104
+ result: { type: 'String', singleton: true }
105
+ },
106
+ {
107
+ name: 'toString-date',
108
+ input: { type: 'Date', singleton: true },
109
+ parameters: [],
110
+ result: { type: 'String', singleton: true }
111
+ },
112
+ {
113
+ name: 'toString-datetime',
114
+ input: { type: 'DateTime', singleton: true },
115
+ parameters: [],
116
+ result: { type: 'String', singleton: true }
117
+ },
118
+ {
119
+ name: 'toString-time',
120
+ input: { type: 'Time', singleton: true },
121
+ parameters: [],
122
+ result: { type: 'String', singleton: true }
123
+ },
124
+ {
125
+ name: 'toString-quantity',
126
+ input: { type: 'Quantity', singleton: true },
85
127
  parameters: [],
86
128
  result: { type: 'String', singleton: true }
87
129
  }
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // trace() requires at least a name argument
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // trim() takes no arguments
@@ -1,6 +1,6 @@
1
1
  import type { FunctionDefinition, FunctionEvaluator } from '../types';
2
2
  import { Errors } from '../errors';
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
  // truncate() takes no arguments