@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900

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 (147) hide show
  1. package/README.md +716 -238
  2. package/dist/index.d.ts +226 -120
  3. package/dist/index.js +11552 -5580
  4. package/dist/index.js.map +1 -1
  5. package/package.json +12 -5
  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 +939 -1204
  13. package/src/completion-provider.ts +209 -191
  14. package/src/complex-types/quantity-value.ts +410 -0
  15. package/src/complex-types/temporal.ts +1776 -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 +506 -468
  23. package/src/lexer.ts +192 -211
  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 +99 -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 +744 -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 +132 -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/extension-function.ts +84 -0
  64. package/src/operations/first-function.ts +1 -1
  65. package/src/operations/floor-function.ts +1 -1
  66. package/src/operations/greater-operator.ts +7 -9
  67. package/src/operations/greater-or-equal-operator.ts +7 -9
  68. package/src/operations/highBoundary-function.ts +120 -0
  69. package/src/operations/hourOf-function.ts +66 -0
  70. package/src/operations/iif-function.ts +193 -8
  71. package/src/operations/implies-operator.ts +2 -1
  72. package/src/operations/in-operator.ts +2 -1
  73. package/src/operations/index.ts +43 -0
  74. package/src/operations/indexOf-function.ts +1 -1
  75. package/src/operations/intersect-function.ts +1 -1
  76. package/src/operations/is-function.ts +70 -0
  77. package/src/operations/is-operator.ts +176 -13
  78. package/src/operations/isDistinct-function.ts +2 -1
  79. package/src/operations/join-function.ts +1 -1
  80. package/src/operations/last-function.ts +1 -1
  81. package/src/operations/lastIndexOf-function.ts +85 -0
  82. package/src/operations/length-function.ts +1 -1
  83. package/src/operations/less-operator.ts +8 -9
  84. package/src/operations/less-or-equal-operator.ts +7 -9
  85. package/src/operations/less-than.ts +8 -13
  86. package/src/operations/lowBoundary-function.ts +120 -0
  87. package/src/operations/lower-function.ts +1 -1
  88. package/src/operations/matches-function.ts +86 -0
  89. package/src/operations/matchesFull-function.ts +96 -0
  90. package/src/operations/millisecondOf-function.ts +66 -0
  91. package/src/operations/minus-operator.ts +76 -4
  92. package/src/operations/minuteOf-function.ts +66 -0
  93. package/src/operations/mod-operator.ts +8 -2
  94. package/src/operations/monthOf-function.ts +66 -0
  95. package/src/operations/multiply-operator.ts +27 -3
  96. package/src/operations/not-equal-operator.ts +24 -30
  97. package/src/operations/not-equivalent-operator.ts +13 -53
  98. package/src/operations/not-function.ts +10 -3
  99. package/src/operations/ofType-function.ts +43 -12
  100. package/src/operations/or-operator.ts +2 -1
  101. package/src/operations/plus-operator.ts +71 -7
  102. package/src/operations/power-function.ts +35 -10
  103. package/src/operations/precision-function.ts +146 -0
  104. package/src/operations/repeat-function.ts +169 -0
  105. package/src/operations/replace-function.ts +1 -1
  106. package/src/operations/replaceMatches-function.ts +125 -0
  107. package/src/operations/round-function.ts +1 -1
  108. package/src/operations/secondOf-function.ts +66 -0
  109. package/src/operations/select-function.ts +66 -5
  110. package/src/operations/single-function.ts +1 -1
  111. package/src/operations/skip-function.ts +1 -1
  112. package/src/operations/split-function.ts +1 -1
  113. package/src/operations/sqrt-function.ts +15 -8
  114. package/src/operations/startsWith-function.ts +1 -1
  115. package/src/operations/subsetOf-function.ts +6 -2
  116. package/src/operations/substring-function.ts +1 -1
  117. package/src/operations/supersetOf-function.ts +6 -2
  118. package/src/operations/tail-function.ts +1 -1
  119. package/src/operations/take-function.ts +1 -1
  120. package/src/operations/temporal-functions.ts +555 -0
  121. package/src/operations/timeOf-function.ts +67 -0
  122. package/src/operations/timezoneOffsetOf-function.ts +69 -0
  123. package/src/operations/toBoolean-function.ts +27 -8
  124. package/src/operations/toChars-function.ts +56 -0
  125. package/src/operations/toDecimal-function.ts +27 -8
  126. package/src/operations/toInteger-function.ts +15 -3
  127. package/src/operations/toLong-function.ts +98 -0
  128. package/src/operations/toQuantity-function.ts +181 -0
  129. package/src/operations/toString-function.ts +78 -15
  130. package/src/operations/trace-function.ts +1 -1
  131. package/src/operations/trim-function.ts +1 -1
  132. package/src/operations/truncate-function.ts +1 -1
  133. package/src/operations/unary-minus-operator.ts +2 -2
  134. package/src/operations/unary-plus-operator.ts +1 -1
  135. package/src/operations/union-function.ts +1 -1
  136. package/src/operations/union-operator.ts +16 -26
  137. package/src/operations/upper-function.ts +1 -1
  138. package/src/operations/where-function.ts +3 -3
  139. package/src/operations/xor-operator.ts +1 -1
  140. package/src/operations/yearOf-function.ts +66 -0
  141. package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
  142. package/src/parser.ts +262 -503
  143. package/src/registry.ts +53 -42
  144. package/src/types.ts +129 -17
  145. package/src/utils/decimal.ts +76 -0
  146. package/src/utils/pprint.ts +151 -0
  147. package/src/quantity-value.ts +0 -198
@@ -0,0 +1,555 @@
1
+ // Built-in temporal functions for FHIRPath
2
+ import type { FunctionDefinition } from '../types';
3
+ import type { FunctionEvaluator } from '../types';
4
+ import { box } from '../interpreter/boxing';
5
+ import { createDate, createDateTime, createTime, isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
6
+
7
+ // ============================================================================
8
+ // now() function - Returns current DateTime
9
+ // ============================================================================
10
+
11
+ export const nowEvaluator: FunctionEvaluator = async (input, context, args) => {
12
+ // Always use cached value if available (set at expression evaluation start)
13
+ const CACHE_KEY = '__fhirpath_now_cache__';
14
+
15
+ if (context.variables[CACHE_KEY]) {
16
+ return {
17
+ value: [context.variables[CACHE_KEY]],
18
+ context
19
+ };
20
+ }
21
+
22
+ // Fallback: create new value if not cached (shouldn't happen in normal evaluation)
23
+ const now = new Date();
24
+ const dateTime = createDateTime(
25
+ now.getFullYear(),
26
+ now.getMonth() + 1,
27
+ now.getDate(),
28
+ now.getHours(),
29
+ now.getMinutes(),
30
+ now.getSeconds(),
31
+ now.getMilliseconds(),
32
+ -now.getTimezoneOffset() // Convert to minutes from UTC (JS gives minutes to subtract from UTC)
33
+ );
34
+
35
+ return {
36
+ value: [box(dateTime, { type: 'DateTime', singleton: true })],
37
+ context
38
+ };
39
+ };
40
+
41
+ export const nowFunction: FunctionDefinition & { evaluate: typeof nowEvaluator } = {
42
+ name: 'now',
43
+ category: ['temporal'],
44
+ description: 'Returns the current date and time as a DateTime value',
45
+ examples: ['now()', 'Patient.birthDate < now()'],
46
+ signatures: [
47
+ {
48
+ name: 'now',
49
+ input: { type: 'Any', singleton: false },
50
+ parameters: [],
51
+ result: { type: 'DateTime', singleton: true }
52
+ }
53
+ ],
54
+ evaluate: nowEvaluator
55
+ };
56
+
57
+ // ============================================================================
58
+ // today() function - Returns current Date
59
+ // ============================================================================
60
+
61
+ export const todayEvaluator: FunctionEvaluator = async (input, context, args) => {
62
+ // Always use cached value if available (set at expression evaluation start)
63
+ const CACHE_KEY = '__fhirpath_today_cache__';
64
+
65
+ if (context.variables[CACHE_KEY]) {
66
+ return {
67
+ value: [context.variables[CACHE_KEY]],
68
+ context
69
+ };
70
+ }
71
+
72
+ // Fallback: create new value if not cached (shouldn't happen in normal evaluation)
73
+ const today = new Date();
74
+ const date = createDate(
75
+ today.getFullYear(),
76
+ today.getMonth() + 1,
77
+ today.getDate()
78
+ );
79
+
80
+ return {
81
+ value: [box(date, { type: 'Date', singleton: true })],
82
+ context
83
+ };
84
+ };
85
+
86
+ export const todayFunction: FunctionDefinition & { evaluate: typeof todayEvaluator } = {
87
+ name: 'today',
88
+ category: ['temporal'],
89
+ description: 'Returns the current date (no time component)',
90
+ examples: ['today()', 'Patient.birthDate < today()'],
91
+ signatures: [
92
+ {
93
+ name: 'today',
94
+ input: { type: 'Any', singleton: false },
95
+ parameters: [],
96
+ result: { type: 'Date', singleton: true }
97
+ }
98
+ ],
99
+ evaluate: todayEvaluator
100
+ };
101
+
102
+ // ============================================================================
103
+ // timeOfDay() function - Returns current Time
104
+ // ============================================================================
105
+
106
+ export const timeOfDayEvaluator: FunctionEvaluator = async (input, context, args) => {
107
+ // Always use cached value if available (set at expression evaluation start)
108
+ const CACHE_KEY = '__fhirpath_timeOfDay_cache__';
109
+
110
+ if (context.variables[CACHE_KEY]) {
111
+ return {
112
+ value: [context.variables[CACHE_KEY]],
113
+ context
114
+ };
115
+ }
116
+
117
+ // Fallback: create new value if not cached (shouldn't happen in normal evaluation)
118
+ const now = new Date();
119
+ const time = createTime(
120
+ now.getHours(),
121
+ now.getMinutes(),
122
+ now.getSeconds(),
123
+ now.getMilliseconds()
124
+ );
125
+
126
+ return {
127
+ value: [box(time, { type: 'Time', singleton: true })],
128
+ context
129
+ };
130
+ };
131
+
132
+ export const timeOfDayFunction: FunctionDefinition & { evaluate: typeof timeOfDayEvaluator } = {
133
+ name: 'timeOfDay',
134
+ category: ['temporal'],
135
+ description: 'Returns the current time of day',
136
+ examples: ['timeOfDay()', '@T09:00 < timeOfDay()'],
137
+ signatures: [
138
+ {
139
+ name: 'timeOfDay',
140
+ input: { type: 'Any', singleton: false },
141
+ parameters: [],
142
+ result: { type: 'Time', singleton: true }
143
+ }
144
+ ],
145
+ evaluate: timeOfDayEvaluator
146
+ };
147
+
148
+ // ============================================================================
149
+ // toDate() function - Convert to Date
150
+ // ============================================================================
151
+
152
+ export const toDateEvaluator: FunctionEvaluator = async (input, context, args) => {
153
+ if (input.length === 0) {
154
+ return { value: [], context };
155
+ }
156
+
157
+ const boxedItem = input[0];
158
+ if (!boxedItem) {
159
+ return { value: [], context };
160
+ }
161
+
162
+ const item = boxedItem.value;
163
+ const typeInfo = boxedItem.typeInfo;
164
+
165
+ // If it's already a Date, return as is
166
+ if (typeInfo?.type === 'Date') {
167
+ return { value: [boxedItem], context };
168
+ }
169
+
170
+ // If it's a DateTime, extract the date portion
171
+ if (typeInfo?.type === 'DateTime' && item && typeof item === 'object') {
172
+ const dateTime = item as any;
173
+ const date = createDate(dateTime.year, dateTime.month, dateTime.day);
174
+ return {
175
+ value: [box(date, { type: 'Date', singleton: true })],
176
+ context
177
+ };
178
+ }
179
+
180
+ // If it's a String, try to parse it
181
+ if (typeof item === 'string') {
182
+ try {
183
+ const { parseTemporalLiteral, isFHIRDate } = await import('../complex-types/temporal');
184
+ const temporal = parseTemporalLiteral('@' + item);
185
+ if (isFHIRDate(temporal)) {
186
+ return {
187
+ value: [box(temporal, { type: 'Date', singleton: true })],
188
+ context
189
+ };
190
+ }
191
+ } catch {
192
+ // Parsing failed, return empty
193
+ }
194
+ }
195
+
196
+ // For any other type (including Boolean), return empty
197
+ // This allows expressions like ({}).empty().toDate() to return []
198
+ return { value: [], context };
199
+ };
200
+
201
+ export const toDateFunction: FunctionDefinition & { evaluate: typeof toDateEvaluator } = {
202
+ name: 'toDate',
203
+ category: ['conversion', 'temporal'],
204
+ description: 'Converts the input to a Date value',
205
+ examples: ["'2020-01-15'.toDate()", '@2020-01-15T10:30:00.toDate()'],
206
+ signatures: [
207
+ {
208
+ name: 'any-toDate',
209
+ parameters: [],
210
+ input: { type: 'Any', singleton: true },
211
+ result: { type: 'Date', singleton: true }
212
+ }
213
+ ],
214
+ evaluate: toDateEvaluator
215
+ };
216
+
217
+ // ============================================================================
218
+ // toDateTime() function - Convert to DateTime
219
+ // ============================================================================
220
+
221
+ export const toDateTimeEvaluator: FunctionEvaluator = async (input, context, args) => {
222
+ if (input.length === 0) {
223
+ return { value: [], context };
224
+ }
225
+
226
+ const boxedItem = input[0];
227
+ if (!boxedItem) {
228
+ return { value: [], context };
229
+ }
230
+
231
+ const item = boxedItem.value;
232
+ const typeInfo = boxedItem.typeInfo;
233
+
234
+ // If it's already a DateTime, return as is
235
+ if (typeInfo?.type === 'DateTime') {
236
+ return { value: [boxedItem], context };
237
+ }
238
+
239
+ // If it's a Date, convert to DateTime with time as 00:00:00
240
+ if (typeInfo?.type === 'Date' && item && typeof item === 'object') {
241
+ const date = item as any;
242
+ const dateTime = createDateTime(
243
+ date.year,
244
+ date.month,
245
+ date.day,
246
+ 0, 0, 0, 0
247
+ );
248
+ return {
249
+ value: [box(dateTime, { type: 'DateTime', singleton: true })],
250
+ context
251
+ };
252
+ }
253
+
254
+ // If it's a String, try to parse it
255
+ if (typeof item === 'string') {
256
+ try {
257
+ const { parseTemporalLiteral, isFHIRDate, isFHIRDateTime } = await import('../complex-types/temporal');
258
+ const temporal = parseTemporalLiteral('@' + item);
259
+ if (isFHIRDateTime(temporal)) {
260
+ return {
261
+ value: [box(temporal, { type: 'DateTime', singleton: true })],
262
+ context
263
+ };
264
+ } else if (isFHIRDate(temporal)) {
265
+ // Convert Date to DateTime (with time as 00:00:00)
266
+ const dateTime = createDateTime(
267
+ temporal.year,
268
+ temporal.month,
269
+ temporal.day,
270
+ 0, 0, 0, 0
271
+ );
272
+ return {
273
+ value: [box(dateTime, { type: 'DateTime', singleton: true })],
274
+ context
275
+ };
276
+ }
277
+ } catch {
278
+ // Parsing failed, return empty
279
+ }
280
+ }
281
+
282
+ // For any other type (including Boolean), return empty
283
+ // This allows expressions like ({}).empty().toDateTime() to return []
284
+ return { value: [], context };
285
+ };
286
+
287
+ export const toDateTimeFunction: FunctionDefinition & { evaluate: typeof toDateTimeEvaluator } = {
288
+ name: 'toDateTime',
289
+ category: ['conversion', 'temporal'],
290
+ description: 'Converts the input to a DateTime value',
291
+ examples: ["'2020-01-15T10:30:00Z'.toDateTime()", '@2020-01-15.toDateTime()'],
292
+ signatures: [
293
+ {
294
+ name: 'any-toDateTime',
295
+ parameters: [],
296
+ input: { type: 'Any', singleton: true },
297
+ result: { type: 'DateTime', singleton: true }
298
+ }
299
+ ],
300
+ evaluate: toDateTimeEvaluator
301
+ };
302
+
303
+ // ============================================================================
304
+ // toTime() function - Convert to Time
305
+ // ============================================================================
306
+
307
+ export const toTimeEvaluator: FunctionEvaluator = async (input, context, args) => {
308
+ if (input.length === 0) {
309
+ return { value: [], context };
310
+ }
311
+
312
+ const boxedItem = input[0];
313
+ if (!boxedItem) {
314
+ return { value: [], context };
315
+ }
316
+
317
+ const item = boxedItem.value;
318
+ const typeInfo = boxedItem.typeInfo;
319
+
320
+ // If it's already a Time, return as is
321
+ if (typeInfo?.type === 'Time') {
322
+ return { value: [boxedItem], context };
323
+ }
324
+
325
+ // If it's a DateTime, extract the time portion
326
+ if (typeInfo?.type === 'DateTime' && item && typeof item === 'object') {
327
+ const dateTime = item as any;
328
+ if (dateTime.hour !== undefined) {
329
+ const time = createTime(dateTime.hour, dateTime.minute, dateTime.second, dateTime.millisecond);
330
+ return {
331
+ value: [box(time, { type: 'Time', singleton: true })],
332
+ context
333
+ };
334
+ }
335
+ }
336
+
337
+ // If it's a String, try to parse it
338
+ if (typeof item === 'string') {
339
+ try {
340
+ const { parseTemporalLiteral, isFHIRTime } = await import('../complex-types/temporal');
341
+ // For time strings, prepend T if not present
342
+ const timeString = item.startsWith('T') ? '@' + item : '@T' + item;
343
+ const temporal = parseTemporalLiteral(timeString);
344
+ if (isFHIRTime(temporal)) {
345
+ return {
346
+ value: [box(temporal, { type: 'Time', singleton: true })],
347
+ context
348
+ };
349
+ }
350
+ } catch {
351
+ // Parsing failed, return empty
352
+ }
353
+ }
354
+
355
+ // For any other type (including Boolean), return empty
356
+ // This allows expressions like ({}).empty().toTime() to return []
357
+ return { value: [], context };
358
+ };
359
+
360
+ export const toTimeFunction: FunctionDefinition & { evaluate: typeof toTimeEvaluator } = {
361
+ name: 'toTime',
362
+ category: ['conversion', 'temporal'],
363
+ description: 'Converts the input to a Time value',
364
+ examples: ["'14:30:00'.toTime()", '@2020-01-15T10:30:00.toTime()'],
365
+ signatures: [
366
+ {
367
+ name: 'any-toTime',
368
+ parameters: [],
369
+ input: { type: 'Any', singleton: true },
370
+ result: { type: 'Time', singleton: true }
371
+ }
372
+ ],
373
+ evaluate: toTimeEvaluator
374
+ };
375
+
376
+ // ============================================================================
377
+ // convertsToDate() function - Check if convertible to Date
378
+ // ============================================================================
379
+
380
+ export const convertsToDateEvaluator: FunctionEvaluator = async (input, context, args) => {
381
+ if (input.length === 0) {
382
+ return { value: [], context };
383
+ }
384
+
385
+ const boxedItem = input[0];
386
+ if (!boxedItem) {
387
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
388
+ }
389
+
390
+ const item = boxedItem.value;
391
+ const typeInfo = boxedItem.typeInfo;
392
+
393
+ // Already a Date
394
+ if (typeInfo?.type === 'Date') {
395
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
396
+ }
397
+
398
+ // DateTime can be converted
399
+ if (typeInfo?.type === 'DateTime') {
400
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
401
+ }
402
+
403
+ // Try to parse string
404
+ if (typeof item === 'string') {
405
+ try {
406
+ const { parseTemporalLiteral, isFHIRDate } = await import('../complex-types/temporal');
407
+ const temporal = parseTemporalLiteral('@' + item);
408
+ return {
409
+ value: [box(isFHIRDate(temporal), { type: 'Boolean', singleton: true })],
410
+ context
411
+ };
412
+ } catch {
413
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
414
+ }
415
+ }
416
+
417
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
418
+ };
419
+
420
+ export const convertsToDateFunction: FunctionDefinition & { evaluate: typeof convertsToDateEvaluator } = {
421
+ name: 'convertsToDate',
422
+ category: ['conversion', 'temporal'],
423
+ description: 'Returns true if the input can be converted to a Date',
424
+ examples: ["'2020-01-15'.convertsToDate()", "'invalid'.convertsToDate()"],
425
+ signatures: [
426
+ {
427
+ name: 'convertsToDate',
428
+ parameters: [],
429
+ input: { type: 'Any', singleton: true },
430
+ result: { type: 'Boolean', singleton: true }
431
+ }
432
+ ],
433
+ evaluate: convertsToDateEvaluator
434
+ };
435
+
436
+ // ============================================================================
437
+ // convertsToDateTime() function - Check if convertible to DateTime
438
+ // ============================================================================
439
+
440
+ export const convertsToDateTimeEvaluator: FunctionEvaluator = async (input, context, args) => {
441
+ if (input.length === 0) {
442
+ return { value: [], context };
443
+ }
444
+
445
+ const boxedItem = input[0];
446
+ if (!boxedItem) {
447
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
448
+ }
449
+
450
+ const item = boxedItem.value;
451
+ const typeInfo = boxedItem.typeInfo;
452
+
453
+ // Already a DateTime
454
+ if (typeInfo?.type === 'DateTime') {
455
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
456
+ }
457
+
458
+ // Date can be converted
459
+ if (typeInfo?.type === 'Date') {
460
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
461
+ }
462
+
463
+ // Try to parse string
464
+ if (typeof item === 'string') {
465
+ try {
466
+ const { parseTemporalLiteral, isFHIRDate, isFHIRDateTime } = await import('../complex-types/temporal');
467
+ const temporal = parseTemporalLiteral('@' + item);
468
+ return {
469
+ value: [box(isFHIRDateTime(temporal) || isFHIRDate(temporal), { type: 'Boolean', singleton: true })],
470
+ context
471
+ };
472
+ } catch {
473
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
474
+ }
475
+ }
476
+
477
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
478
+ };
479
+
480
+ export const convertsToDateTimeFunction: FunctionDefinition & { evaluate: typeof convertsToDateTimeEvaluator } = {
481
+ name: 'convertsToDateTime',
482
+ category: ['conversion', 'temporal'],
483
+ description: 'Returns true if the input can be converted to a DateTime',
484
+ examples: ["'2020-01-15T10:30:00'.convertsToDateTime()", "'invalid'.convertsToDateTime()"],
485
+ signatures: [
486
+ {
487
+ name: 'convertsToDateTime',
488
+ parameters: [],
489
+ input: { type: 'Any', singleton: true },
490
+ result: { type: 'Boolean', singleton: true }
491
+ }
492
+ ],
493
+ evaluate: convertsToDateTimeEvaluator
494
+ };
495
+
496
+ // ============================================================================
497
+ // convertsToTime() function - Check if convertible to Time
498
+ // ============================================================================
499
+
500
+ export const convertsToTimeEvaluator: FunctionEvaluator = async (input, context, args) => {
501
+ if (input.length === 0) {
502
+ return { value: [], context };
503
+ }
504
+
505
+ const boxedItem = input[0];
506
+ if (!boxedItem) {
507
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
508
+ }
509
+
510
+ const item = boxedItem.value;
511
+ const typeInfo = boxedItem.typeInfo;
512
+
513
+ // Already a Time
514
+ if (typeInfo?.type === 'Time') {
515
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
516
+ }
517
+
518
+ // DateTime can be converted if it has time components
519
+ if (typeInfo?.type === 'DateTime') {
520
+ return { value: [box(true, { type: 'Boolean', singleton: true })], context };
521
+ }
522
+
523
+ // Try to parse string
524
+ if (typeof item === 'string') {
525
+ try {
526
+ const { parseTemporalLiteral, isFHIRTime } = await import('../complex-types/temporal');
527
+ const timeString = item.startsWith('T') ? '@' + item : '@T' + item;
528
+ const temporal = parseTemporalLiteral(timeString);
529
+ return {
530
+ value: [box(isFHIRTime(temporal), { type: 'Boolean', singleton: true })],
531
+ context
532
+ };
533
+ } catch {
534
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
535
+ }
536
+ }
537
+
538
+ return { value: [box(false, { type: 'Boolean', singleton: true })], context };
539
+ };
540
+
541
+ export const convertsToTimeFunction: FunctionDefinition & { evaluate: typeof convertsToTimeEvaluator } = {
542
+ name: 'convertsToTime',
543
+ category: ['conversion', 'temporal'],
544
+ description: 'Returns true if the input can be converted to a Time',
545
+ examples: ["'14:30:00'.convertsToTime()", "'invalid'.convertsToTime()"],
546
+ signatures: [
547
+ {
548
+ name: 'convertsToTime',
549
+ parameters: [],
550
+ input: { type: 'Any', singleton: true },
551
+ result: { type: 'Boolean', singleton: true }
552
+ }
553
+ ],
554
+ evaluate: convertsToTimeEvaluator
555
+ };
@@ -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
+ };