fhirpath-ts 0.0.0-bootstrap.0

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 (414) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +391 -0
  3. package/THIRD-PARTY-NOTICES.md +296 -0
  4. package/dist/analyzer/analyze-dto.d.ts +45 -0
  5. package/dist/analyzer/analyze-dto.d.ts.map +1 -0
  6. package/dist/analyzer/analyze-dto.js +147 -0
  7. package/dist/analyzer/analyze-dto.js.map +1 -0
  8. package/dist/analyzer/analyze.d.ts +98 -0
  9. package/dist/analyzer/analyze.d.ts.map +1 -0
  10. package/dist/analyzer/analyze.js +925 -0
  11. package/dist/analyzer/analyze.js.map +1 -0
  12. package/dist/analyzer/expression-policy.d.ts +210 -0
  13. package/dist/analyzer/expression-policy.d.ts.map +1 -0
  14. package/dist/analyzer/expression-policy.js +254 -0
  15. package/dist/analyzer/expression-policy.js.map +1 -0
  16. package/dist/analyzer/index.d.ts +3 -0
  17. package/dist/analyzer/index.d.ts.map +1 -0
  18. package/dist/analyzer/index.js +6 -0
  19. package/dist/analyzer/index.js.map +1 -0
  20. package/dist/analyzer/regex-safety.d.ts +10 -0
  21. package/dist/analyzer/regex-safety.d.ts.map +1 -0
  22. package/dist/analyzer/regex-safety.js +84 -0
  23. package/dist/analyzer/regex-safety.js.map +1 -0
  24. package/dist/analyzer/signatures.d.ts +72 -0
  25. package/dist/analyzer/signatures.d.ts.map +1 -0
  26. package/dist/analyzer/signatures.js +195 -0
  27. package/dist/analyzer/signatures.js.map +1 -0
  28. package/dist/api/bundle.d.ts +29 -0
  29. package/dist/api/bundle.d.ts.map +1 -0
  30. package/dist/api/bundle.js +108 -0
  31. package/dist/api/bundle.js.map +1 -0
  32. package/dist/api/cache.d.ts +10 -0
  33. package/dist/api/cache.d.ts.map +1 -0
  34. package/dist/api/cache.js +38 -0
  35. package/dist/api/cache.js.map +1 -0
  36. package/dist/api/column-signature.d.ts +53 -0
  37. package/dist/api/column-signature.d.ts.map +1 -0
  38. package/dist/api/column-signature.js +43 -0
  39. package/dist/api/column-signature.js.map +1 -0
  40. package/dist/api/compile.d.ts +120 -0
  41. package/dist/api/compile.d.ts.map +1 -0
  42. package/dist/api/compile.js +167 -0
  43. package/dist/api/compile.js.map +1 -0
  44. package/dist/api/constraints.d.ts +48 -0
  45. package/dist/api/constraints.d.ts.map +1 -0
  46. package/dist/api/constraints.js +52 -0
  47. package/dist/api/constraints.js.map +1 -0
  48. package/dist/api/dto.d.ts +134 -0
  49. package/dist/api/dto.d.ts.map +1 -0
  50. package/dist/api/dto.js +295 -0
  51. package/dist/api/dto.js.map +1 -0
  52. package/dist/api/engine.d.ts +136 -0
  53. package/dist/api/engine.d.ts.map +1 -0
  54. package/dist/api/engine.js +183 -0
  55. package/dist/api/engine.js.map +1 -0
  56. package/dist/api/evaluate.d.ts +7 -0
  57. package/dist/api/evaluate.d.ts.map +1 -0
  58. package/dist/api/evaluate.js +10 -0
  59. package/dist/api/evaluate.js.map +1 -0
  60. package/dist/api/project.d.ts +89 -0
  61. package/dist/api/project.d.ts.map +1 -0
  62. package/dist/api/project.js +150 -0
  63. package/dist/api/project.js.map +1 -0
  64. package/dist/api/tagged.d.ts +14 -0
  65. package/dist/api/tagged.d.ts.map +1 -0
  66. package/dist/api/tagged.js +13 -0
  67. package/dist/api/tagged.js.map +1 -0
  68. package/dist/cli/dto-check.d.ts +25 -0
  69. package/dist/cli/dto-check.d.ts.map +1 -0
  70. package/dist/cli/dto-check.js +77 -0
  71. package/dist/cli/dto-check.js.map +1 -0
  72. package/dist/cli/fhirpath-check.d.ts +3 -0
  73. package/dist/cli/fhirpath-check.d.ts.map +1 -0
  74. package/dist/cli/fhirpath-check.js +149 -0
  75. package/dist/cli/fhirpath-check.js.map +1 -0
  76. package/dist/cli/ts-loader.d.mts +3 -0
  77. package/dist/cli/ts-loader.d.mts.map +1 -0
  78. package/dist/cli/ts-loader.mjs +48 -0
  79. package/dist/cli/ts-loader.mjs.map +1 -0
  80. package/dist/engine/context.d.ts +173 -0
  81. package/dist/engine/context.d.ts.map +1 -0
  82. package/dist/engine/context.js +118 -0
  83. package/dist/engine/context.js.map +1 -0
  84. package/dist/engine/evaluator.d.ts +7 -0
  85. package/dist/engine/evaluator.d.ts.map +1 -0
  86. package/dist/engine/evaluator.js +196 -0
  87. package/dist/engine/evaluator.js.map +1 -0
  88. package/dist/engine/navigation.d.ts +11 -0
  89. package/dist/engine/navigation.d.ts.map +1 -0
  90. package/dist/engine/navigation.js +102 -0
  91. package/dist/engine/navigation.js.map +1 -0
  92. package/dist/engine/operators/collections.d.ts +12 -0
  93. package/dist/engine/operators/collections.d.ts.map +1 -0
  94. package/dist/engine/operators/collections.js +31 -0
  95. package/dist/engine/operators/collections.js.map +1 -0
  96. package/dist/engine/operators/comparison.d.ts +13 -0
  97. package/dist/engine/operators/comparison.d.ts.map +1 -0
  98. package/dist/engine/operators/comparison.js +77 -0
  99. package/dist/engine/operators/comparison.js.map +1 -0
  100. package/dist/engine/operators/equality.d.ts +19 -0
  101. package/dist/engine/operators/equality.d.ts.map +1 -0
  102. package/dist/engine/operators/equality.js +203 -0
  103. package/dist/engine/operators/equality.js.map +1 -0
  104. package/dist/engine/operators/index.d.ts +18 -0
  105. package/dist/engine/operators/index.d.ts.map +1 -0
  106. package/dist/engine/operators/index.js +30 -0
  107. package/dist/engine/operators/index.js.map +1 -0
  108. package/dist/engine/operators/logic.d.ts +8 -0
  109. package/dist/engine/operators/logic.d.ts.map +1 -0
  110. package/dist/engine/operators/logic.js +38 -0
  111. package/dist/engine/operators/logic.js.map +1 -0
  112. package/dist/engine/operators/math.d.ts +13 -0
  113. package/dist/engine/operators/math.d.ts.map +1 -0
  114. package/dist/engine/operators/math.js +198 -0
  115. package/dist/engine/operators/math.js.map +1 -0
  116. package/dist/engine/operators/strings.d.ts +8 -0
  117. package/dist/engine/operators/strings.d.ts.map +1 -0
  118. package/dist/engine/operators/strings.js +22 -0
  119. package/dist/engine/operators/strings.js.map +1 -0
  120. package/dist/engine/operators/types.d.ts +3 -0
  121. package/dist/engine/operators/types.d.ts.map +1 -0
  122. package/dist/engine/operators/types.js +15 -0
  123. package/dist/engine/operators/types.js.map +1 -0
  124. package/dist/engine/type-matching.d.ts +20 -0
  125. package/dist/engine/type-matching.d.ts.map +1 -0
  126. package/dist/engine/type-matching.js +122 -0
  127. package/dist/engine/type-matching.js.map +1 -0
  128. package/dist/errors.d.ts +25 -0
  129. package/dist/errors.d.ts.map +1 -0
  130. package/dist/errors.js +21 -0
  131. package/dist/errors.js.map +1 -0
  132. package/dist/eslint/index.d.ts +12 -0
  133. package/dist/eslint/index.d.ts.map +1 -0
  134. package/dist/eslint/index.js +316 -0
  135. package/dist/eslint/index.js.map +1 -0
  136. package/dist/fhir/html-checks.d.ts +7 -0
  137. package/dist/fhir/html-checks.d.ts.map +1 -0
  138. package/dist/fhir/html-checks.js +256 -0
  139. package/dist/fhir/html-checks.js.map +1 -0
  140. package/dist/fhir/model-navigation.d.ts +11 -0
  141. package/dist/fhir/model-navigation.d.ts.map +1 -0
  142. package/dist/fhir/model-navigation.js +142 -0
  143. package/dist/fhir/model-navigation.js.map +1 -0
  144. package/dist/functions/aggregate.d.ts +2 -0
  145. package/dist/functions/aggregate.d.ts.map +1 -0
  146. package/dist/functions/aggregate.js +127 -0
  147. package/dist/functions/aggregate.js.map +1 -0
  148. package/dist/functions/boundary.d.ts +2 -0
  149. package/dist/functions/boundary.d.ts.map +1 -0
  150. package/dist/functions/boundary.js +189 -0
  151. package/dist/functions/boundary.js.map +1 -0
  152. package/dist/functions/combining.d.ts +2 -0
  153. package/dist/functions/combining.d.ts.map +1 -0
  154. package/dist/functions/combining.js +13 -0
  155. package/dist/functions/combining.js.map +1 -0
  156. package/dist/functions/control.d.ts +2 -0
  157. package/dist/functions/control.d.ts.map +1 -0
  158. package/dist/functions/control.js +25 -0
  159. package/dist/functions/control.js.map +1 -0
  160. package/dist/functions/conversion.d.ts +3 -0
  161. package/dist/functions/conversion.d.ts.map +1 -0
  162. package/dist/functions/conversion.js +316 -0
  163. package/dist/functions/conversion.js.map +1 -0
  164. package/dist/functions/date-components.d.ts +2 -0
  165. package/dist/functions/date-components.d.ts.map +1 -0
  166. package/dist/functions/date-components.js +81 -0
  167. package/dist/functions/date-components.js.map +1 -0
  168. package/dist/functions/existence.d.ts +4 -0
  169. package/dist/functions/existence.d.ts.map +1 -0
  170. package/dist/functions/existence.js +97 -0
  171. package/dist/functions/existence.js.map +1 -0
  172. package/dist/functions/fhir.d.ts +2 -0
  173. package/dist/functions/fhir.d.ts.map +1 -0
  174. package/dist/functions/fhir.js +222 -0
  175. package/dist/functions/fhir.js.map +1 -0
  176. package/dist/functions/filtering.d.ts +2 -0
  177. package/dist/functions/filtering.d.ts.map +1 -0
  178. package/dist/functions/filtering.js +112 -0
  179. package/dist/functions/filtering.js.map +1 -0
  180. package/dist/functions/install.d.ts +18 -0
  181. package/dist/functions/install.d.ts.map +1 -0
  182. package/dist/functions/install.js +20 -0
  183. package/dist/functions/install.js.map +1 -0
  184. package/dist/functions/iteration.d.ts +7 -0
  185. package/dist/functions/iteration.d.ts.map +1 -0
  186. package/dist/functions/iteration.js +9 -0
  187. package/dist/functions/iteration.js.map +1 -0
  188. package/dist/functions/math.d.ts +2 -0
  189. package/dist/functions/math.d.ts.map +1 -0
  190. package/dist/functions/math.js +152 -0
  191. package/dist/functions/math.js.map +1 -0
  192. package/dist/functions/reflection.d.ts +2 -0
  193. package/dist/functions/reflection.d.ts.map +1 -0
  194. package/dist/functions/reflection.js +23 -0
  195. package/dist/functions/reflection.js.map +1 -0
  196. package/dist/functions/registry.d.ts +28 -0
  197. package/dist/functions/registry.d.ts.map +1 -0
  198. package/dist/functions/registry.js +46 -0
  199. package/dist/functions/registry.js.map +1 -0
  200. package/dist/functions/sort.d.ts +2 -0
  201. package/dist/functions/sort.d.ts.map +1 -0
  202. package/dist/functions/sort.js +48 -0
  203. package/dist/functions/sort.js.map +1 -0
  204. package/dist/functions/string.d.ts +2 -0
  205. package/dist/functions/string.d.ts.map +1 -0
  206. package/dist/functions/string.js +353 -0
  207. package/dist/functions/string.js.map +1 -0
  208. package/dist/functions/subsetting.d.ts +2 -0
  209. package/dist/functions/subsetting.d.ts.map +1 -0
  210. package/dist/functions/subsetting.js +72 -0
  211. package/dist/functions/subsetting.js.map +1 -0
  212. package/dist/functions/tree.d.ts +8 -0
  213. package/dist/functions/tree.d.ts.map +1 -0
  214. package/dist/functions/tree.js +109 -0
  215. package/dist/functions/tree.js.map +1 -0
  216. package/dist/functions/type-specifier.d.ts +8 -0
  217. package/dist/functions/type-specifier.d.ts.map +1 -0
  218. package/dist/functions/type-specifier.js +27 -0
  219. package/dist/functions/type-specifier.js.map +1 -0
  220. package/dist/functions/utility.d.ts +2 -0
  221. package/dist/functions/utility.d.ts.map +1 -0
  222. package/dist/functions/utility.js +76 -0
  223. package/dist/functions/utility.js.map +1 -0
  224. package/dist/functions/variables.d.ts +2 -0
  225. package/dist/functions/variables.d.ts.map +1 -0
  226. package/dist/functions/variables.js +35 -0
  227. package/dist/functions/variables.js.map +1 -0
  228. package/dist/index.d.ts +25 -0
  229. package/dist/index.d.ts.map +1 -0
  230. package/dist/index.js +11 -0
  231. package/dist/index.js.map +1 -0
  232. package/dist/lexer/lexer.d.ts +4 -0
  233. package/dist/lexer/lexer.d.ts.map +1 -0
  234. package/dist/lexer/lexer.js +318 -0
  235. package/dist/lexer/lexer.js.map +1 -0
  236. package/dist/lexer/tokens.d.ts +23 -0
  237. package/dist/lexer/tokens.d.ts.map +1 -0
  238. package/dist/lexer/tokens.js +40 -0
  239. package/dist/lexer/tokens.js.map +1 -0
  240. package/dist/model/provider.d.ts +36 -0
  241. package/dist/model/provider.d.ts.map +1 -0
  242. package/dist/model/provider.js +2 -0
  243. package/dist/model/provider.js.map +1 -0
  244. package/dist/parser/ast.d.ts +104 -0
  245. package/dist/parser/ast.d.ts.map +1 -0
  246. package/dist/parser/ast.js +2 -0
  247. package/dist/parser/ast.js.map +1 -0
  248. package/dist/parser/parser.d.ts +4 -0
  249. package/dist/parser/parser.d.ts.map +1 -0
  250. package/dist/parser/parser.js +299 -0
  251. package/dist/parser/parser.js.map +1 -0
  252. package/dist/parser/precedence.d.ts +24 -0
  253. package/dist/parser/precedence.d.ts.map +1 -0
  254. package/dist/parser/precedence.js +52 -0
  255. package/dist/parser/precedence.js.map +1 -0
  256. package/dist/parser/printer.d.ts +4 -0
  257. package/dist/parser/printer.d.ts.map +1 -0
  258. package/dist/parser/printer.js +112 -0
  259. package/dist/parser/printer.js.map +1 -0
  260. package/dist/r4/generated/resources-data.d.ts +3 -0
  261. package/dist/r4/generated/resources-data.d.ts.map +1 -0
  262. package/dist/r4/generated/resources-data.js +28628 -0
  263. package/dist/r4/generated/resources-data.js.map +1 -0
  264. package/dist/r4/generated/type-maps.d.ts +43153 -0
  265. package/dist/r4/generated/type-maps.d.ts.map +1 -0
  266. package/dist/r4/generated/type-maps.js +4 -0
  267. package/dist/r4/generated/type-maps.js.map +1 -0
  268. package/dist/r4/generated/types-data.d.ts +3 -0
  269. package/dist/r4/generated/types-data.d.ts.map +1 -0
  270. package/dist/r4/generated/types-data.js +2205 -0
  271. package/dist/r4/generated/types-data.js.map +1 -0
  272. package/dist/r4/index.d.ts +9 -0
  273. package/dist/r4/index.d.ts.map +1 -0
  274. package/dist/r4/index.js +94 -0
  275. package/dist/r4/index.js.map +1 -0
  276. package/dist/r4/model-data.d.ts +17 -0
  277. package/dist/r4/model-data.d.ts.map +1 -0
  278. package/dist/r4/model-data.js +2 -0
  279. package/dist/r4/model-data.js.map +1 -0
  280. package/dist/sites/index.d.ts +31 -0
  281. package/dist/sites/index.d.ts.map +1 -0
  282. package/dist/sites/index.js +205 -0
  283. package/dist/sites/index.js.map +1 -0
  284. package/dist/typed/infer.d.ts +234 -0
  285. package/dist/typed/infer.d.ts.map +1 -0
  286. package/dist/typed/infer.js +85 -0
  287. package/dist/typed/infer.js.map +1 -0
  288. package/dist/typed/perf-fixture.types.d.ts +136 -0
  289. package/dist/typed/perf-fixture.types.d.ts.map +1 -0
  290. package/dist/typed/perf-fixture.types.js +2 -0
  291. package/dist/typed/perf-fixture.types.js.map +1 -0
  292. package/dist/values/collection.d.ts +24 -0
  293. package/dist/values/collection.d.ts.map +1 -0
  294. package/dist/values/collection.js +51 -0
  295. package/dist/values/collection.js.map +1 -0
  296. package/dist/values/datetime.d.ts +54 -0
  297. package/dist/values/datetime.d.ts.map +1 -0
  298. package/dist/values/datetime.js +226 -0
  299. package/dist/values/datetime.js.map +1 -0
  300. package/dist/values/decimal.d.ts +48 -0
  301. package/dist/values/decimal.d.ts.map +1 -0
  302. package/dist/values/decimal.js +188 -0
  303. package/dist/values/decimal.js.map +1 -0
  304. package/dist/values/numeric.d.ts +24 -0
  305. package/dist/values/numeric.d.ts.map +1 -0
  306. package/dist/values/numeric.js +53 -0
  307. package/dist/values/numeric.js.map +1 -0
  308. package/dist/values/quantity.d.ts +49 -0
  309. package/dist/values/quantity.d.ts.map +1 -0
  310. package/dist/values/quantity.js +297 -0
  311. package/dist/values/quantity.js.map +1 -0
  312. package/dist/values/temporal-arithmetic.d.ts +17 -0
  313. package/dist/values/temporal-arithmetic.d.ts.map +1 -0
  314. package/dist/values/temporal-arithmetic.js +183 -0
  315. package/dist/values/temporal-arithmetic.js.map +1 -0
  316. package/dist/values/temporal-compare.d.ts +9 -0
  317. package/dist/values/temporal-compare.d.ts.map +1 -0
  318. package/dist/values/temporal-compare.js +59 -0
  319. package/dist/values/temporal-compare.js.map +1 -0
  320. package/dist/values/type-compat.d.ts +47 -0
  321. package/dist/values/type-compat.d.ts.map +1 -0
  322. package/dist/values/type-compat.js +100 -0
  323. package/dist/values/type-compat.js.map +1 -0
  324. package/dist/values/typed-value.d.ts +48 -0
  325. package/dist/values/typed-value.d.ts.map +1 -0
  326. package/dist/values/typed-value.js +117 -0
  327. package/dist/values/typed-value.js.map +1 -0
  328. package/dist/values/ucum.d.ts +14 -0
  329. package/dist/values/ucum.d.ts.map +1 -0
  330. package/dist/values/ucum.js +146 -0
  331. package/dist/values/ucum.js.map +1 -0
  332. package/package.json +137 -0
  333. package/src/analyzer/analyze-dto.ts +195 -0
  334. package/src/analyzer/analyze.ts +1181 -0
  335. package/src/analyzer/expression-policy.ts +434 -0
  336. package/src/analyzer/index.ts +23 -0
  337. package/src/analyzer/regex-safety.ts +85 -0
  338. package/src/analyzer/signatures.ts +260 -0
  339. package/src/api/bundle.ts +132 -0
  340. package/src/api/cache.ts +40 -0
  341. package/src/api/column-signature.ts +71 -0
  342. package/src/api/compile.ts +308 -0
  343. package/src/api/constraints.ts +104 -0
  344. package/src/api/dto.ts +445 -0
  345. package/src/api/engine.ts +328 -0
  346. package/src/api/evaluate.ts +11 -0
  347. package/src/api/project.ts +255 -0
  348. package/src/api/tagged.ts +35 -0
  349. package/src/cli/dto-check.ts +94 -0
  350. package/src/cli/fhirpath-check.ts +168 -0
  351. package/src/cli/ts-loader.mjs +49 -0
  352. package/src/engine/context.ts +282 -0
  353. package/src/engine/evaluator.ts +246 -0
  354. package/src/engine/navigation.ts +106 -0
  355. package/src/engine/operators/collections.ts +34 -0
  356. package/src/engine/operators/comparison.ts +82 -0
  357. package/src/engine/operators/equality.ts +232 -0
  358. package/src/engine/operators/index.ts +65 -0
  359. package/src/engine/operators/logic.ts +53 -0
  360. package/src/engine/operators/math.ts +225 -0
  361. package/src/engine/operators/strings.ts +25 -0
  362. package/src/engine/operators/types.ts +16 -0
  363. package/src/engine/type-matching.ts +148 -0
  364. package/src/errors.ts +33 -0
  365. package/src/eslint/index.ts +388 -0
  366. package/src/fhir/html-checks.ts +266 -0
  367. package/src/fhir/model-navigation.ts +148 -0
  368. package/src/functions/aggregate.ts +136 -0
  369. package/src/functions/boundary.ts +209 -0
  370. package/src/functions/combining.ts +15 -0
  371. package/src/functions/control.ts +25 -0
  372. package/src/functions/conversion.ts +349 -0
  373. package/src/functions/date-components.ts +88 -0
  374. package/src/functions/existence.ts +107 -0
  375. package/src/functions/fhir.ts +247 -0
  376. package/src/functions/filtering.ts +120 -0
  377. package/src/functions/install.ts +19 -0
  378. package/src/functions/iteration.ts +21 -0
  379. package/src/functions/math.ts +167 -0
  380. package/src/functions/reflection.ts +24 -0
  381. package/src/functions/registry.ts +72 -0
  382. package/src/functions/sort.ts +68 -0
  383. package/src/functions/string.ts +413 -0
  384. package/src/functions/subsetting.ts +89 -0
  385. package/src/functions/tree.ts +112 -0
  386. package/src/functions/type-specifier.ts +29 -0
  387. package/src/functions/utility.ts +82 -0
  388. package/src/functions/variables.ts +36 -0
  389. package/src/index.ts +30 -0
  390. package/src/lexer/lexer.ts +341 -0
  391. package/src/lexer/tokens.ts +69 -0
  392. package/src/model/provider.ts +36 -0
  393. package/src/parser/ast.ts +164 -0
  394. package/src/parser/parser.ts +321 -0
  395. package/src/parser/precedence.ts +52 -0
  396. package/src/parser/printer.ts +114 -0
  397. package/src/r4/generated/resources-data.ts +28631 -0
  398. package/src/r4/generated/type-maps.ts +22145 -0
  399. package/src/r4/generated/types-data.ts +2208 -0
  400. package/src/r4/index.ts +107 -0
  401. package/src/r4/model-data.ts +17 -0
  402. package/src/sites/index.ts +285 -0
  403. package/src/typed/infer.ts +433 -0
  404. package/src/typed/perf-fixture.types.ts +143 -0
  405. package/src/values/collection.ts +57 -0
  406. package/src/values/datetime.ts +285 -0
  407. package/src/values/decimal.ts +213 -0
  408. package/src/values/numeric.ts +63 -0
  409. package/src/values/quantity.ts +358 -0
  410. package/src/values/temporal-arithmetic.ts +231 -0
  411. package/src/values/temporal-compare.ts +65 -0
  412. package/src/values/type-compat.ts +133 -0
  413. package/src/values/typed-value.ts +151 -0
  414. package/src/values/ucum.ts +181 -0
@@ -0,0 +1,358 @@
1
+ import { Decimal } from './decimal.ts'
2
+ import { asNumeric } from './numeric.ts'
3
+ import { type QuantityValue, SYSTEM_QUANTITY, type TypedValue, typeLocalName } from './typed-value.ts'
4
+ import { canonicalizeUnit, sameDimensions } from './ucum.ts'
5
+
6
+ /** FHIR Quantity and its specializations coerce to System.Quantity for comparison. */
7
+ const FHIR_QUANTITY_TYPES = new Set([
8
+ 'Quantity',
9
+ 'Age',
10
+ 'Distance',
11
+ 'Duration',
12
+ 'Count',
13
+ 'SimpleQuantity',
14
+ 'MoneyQuantity',
15
+ ])
16
+
17
+ /**
18
+ * Calendar duration data used to build every lookup below. Years and months use
19
+ * months; smaller units use seconds. Week-and-smaller UCUM forms are equal to
20
+ * calendar values. Year and month UCUM forms are equivalent only.
21
+ */
22
+ interface CalendarUnit {
23
+ plural: string
24
+ family: 'month' | 'second'
25
+ factor: string
26
+ ucum: string
27
+ exact: boolean
28
+ }
29
+
30
+ const CALENDAR_UNITS: Readonly<Record<string, CalendarUnit>> = {
31
+ year: { plural: 'years', family: 'month', factor: '12', ucum: 'a', exact: false },
32
+ month: { plural: 'months', family: 'month', factor: '1', ucum: 'mo', exact: false },
33
+ week: { plural: 'weeks', family: 'second', factor: '604800', ucum: 'wk', exact: true },
34
+ day: { plural: 'days', family: 'second', factor: '86400', ucum: 'd', exact: true },
35
+ hour: { plural: 'hours', family: 'second', factor: '3600', ucum: 'h', exact: true },
36
+ minute: { plural: 'minutes', family: 'second', factor: '60', ucum: 'min', exact: true },
37
+ second: { plural: 'seconds', family: 'second', factor: '1', ucum: 's', exact: true },
38
+ millisecond: { plural: 'milliseconds', family: 'second', factor: '0.001', ucum: 'ms', exact: true },
39
+ }
40
+
41
+ const CALENDAR_UNIT_ENTRIES = Object.entries(CALENDAR_UNITS)
42
+
43
+ /** Plural keyword → singular (`years` → `year`). */
44
+ const SINGULAR_CALENDAR_UNITS: Readonly<Record<string, string>> = Object.fromEntries(
45
+ CALENDAR_UNIT_ENTRIES.map(([singular, unit]) => [unit.plural, singular])
46
+ )
47
+
48
+ /** Year/month family, factor in months. */
49
+ const MONTH_FAMILY: Readonly<Record<string, string>> = Object.fromEntries(
50
+ CALENDAR_UNIT_ENTRIES.filter(([, unit]) => unit.family === 'month').map(([singular, unit]) => [singular, unit.factor])
51
+ )
52
+
53
+ /** Week-and-below family, factor in seconds (all exact). */
54
+ const SECOND_FAMILY: Readonly<Record<string, string>> = Object.fromEntries(
55
+ CALENDAR_UNIT_ENTRIES.filter(([, unit]) => unit.family === 'second').map(([singular, unit]) => [
56
+ singular,
57
+ unit.factor,
58
+ ])
59
+ )
60
+
61
+ /** Calendar words that equal their UCUM twin (week and below). */
62
+ const CALENDAR_TO_UCUM_EXACT: Readonly<Record<string, string>> = Object.fromEntries(
63
+ CALENDAR_UNIT_ENTRIES.filter(([, unit]) => unit.exact).map(([singular, unit]) => [singular, unit.ucum])
64
+ )
65
+
66
+ /** Every calendar word to its UCUM twin (equivalence-only for year/month). */
67
+ const CALENDAR_TO_UCUM_LOOSE: Readonly<Record<string, string>> = Object.fromEntries(
68
+ CALENDAR_UNIT_ENTRIES.map(([singular, unit]) => [singular, unit.ucum])
69
+ )
70
+
71
+ /**
72
+ * FHIR Quantities with UCUM time-valued codes convert to calendar duration
73
+ * keywords (FHIR fhirpath page, "Use of FHIR Quantity"): a Duration of 1 'a'
74
+ * behaves as `1 year`.
75
+ */
76
+ const UCUM_TIME_TO_CALENDAR: Readonly<Record<string, string>> = Object.fromEntries(
77
+ CALENDAR_UNIT_ENTRIES.map(([singular, unit]) => [unit.ucum, singular])
78
+ )
79
+
80
+ const UCUM_SYSTEM = 'http://unitsofmeasure.org'
81
+
82
+ /** Read a TypedValue as a quantity: System.Quantity directly, FHIR Quantity objects by value+code/unit. */
83
+ export function coerceQuantity(item: TypedValue): QuantityValue | undefined {
84
+ if (item.type === SYSTEM_QUANTITY) {
85
+ return item.value as QuantityValue
86
+ }
87
+ if (!(item.type.startsWith('FHIR.') && FHIR_QUANTITY_TYPES.has(typeLocalName(item.type)))) {
88
+ return undefined
89
+ }
90
+ const raw = item.value as { value?: unknown; code?: unknown; unit?: unknown; system?: unknown }
91
+ if (typeof raw !== 'object' || raw === null || typeof raw.value !== 'number') {
92
+ return undefined
93
+ }
94
+ const value = Decimal.fromNumber(raw.value)
95
+ if (!value) {
96
+ return undefined
97
+ }
98
+ if (raw.system === UCUM_SYSTEM && typeof raw.code === 'string' && UCUM_TIME_TO_CALENDAR[raw.code] !== undefined) {
99
+ return { value, unit: UCUM_TIME_TO_CALENDAR[raw.code] as string, calendar: true }
100
+ }
101
+ const unit = typeof raw.code === 'string' ? raw.code : typeof raw.unit === 'string' ? raw.unit : '1'
102
+ return { value, unit, calendar: false }
103
+ }
104
+
105
+ /** A quantity as-is, or a number implicitly converted to a unity quantity (spec §5.5). */
106
+ export function promoteQuantity(item: TypedValue): QuantityValue | undefined {
107
+ const coerced = coerceQuantity(item)
108
+ if (coerced) {
109
+ return coerced
110
+ }
111
+ const numeric = asNumeric(item)
112
+ return numeric === undefined ? undefined : { value: numeric.value, unit: '1', calendar: false }
113
+ }
114
+
115
+ /**
116
+ * Coerce a pair of operands for quantity operations. Integers, Longs, and
117
+ * Decimals implicitly convert to unity quantities (spec §5.5) when the other
118
+ * side is a quantity: `9 = 9 '1'` and `2 * 4 'kg'` both work.
119
+ */
120
+ export function coerceQuantityPair(a: TypedValue, b: TypedValue): [QuantityValue, QuantityValue] | undefined {
121
+ const quantityA = coerceQuantity(a)
122
+ const quantityB = coerceQuantity(b)
123
+ if (quantityA && quantityB) {
124
+ return [quantityA, quantityB]
125
+ }
126
+ if (quantityA) {
127
+ const numeric = asNumeric(b)
128
+ return numeric ? [quantityA, { value: numeric.value, unit: '1', calendar: false }] : undefined
129
+ }
130
+ if (quantityB) {
131
+ const numeric = asNumeric(a)
132
+ return numeric ? [{ value: numeric.value, unit: '1', calendar: false }, quantityB] : undefined
133
+ }
134
+ return undefined
135
+ }
136
+
137
+ /** `4 days` and `1 day` use the same unit; plurals normalize to the singular word. */
138
+ export function normalizeCalendarUnit(unit: string): string {
139
+ return SINGULAR_CALENDAR_UNITS[unit] ?? unit
140
+ }
141
+
142
+ function calendarFamily(unit: string): { family: 'month' | 'second'; factor: Decimal } | undefined {
143
+ const singular = normalizeCalendarUnit(unit)
144
+ const monthFactor = MONTH_FAMILY[singular]
145
+ if (monthFactor !== undefined) {
146
+ return { family: 'month', factor: Decimal.fromString(monthFactor) as Decimal }
147
+ }
148
+ const secondFactor = SECOND_FAMILY[singular]
149
+ if (secondFactor !== undefined) {
150
+ return { family: 'second', factor: Decimal.fromString(secondFactor) as Decimal }
151
+ }
152
+ /* v8 ignore next 2 -- the lexer only produces the units above */
153
+ return undefined
154
+ }
155
+
156
+ /**
157
+ * Compare two quantities. Undefined when the units are not comparable: different
158
+ * dimensions, opaque units with different spellings, or calendar words above
159
+ * seconds against UCUM time units (those are only equivalent, spec §6.1).
160
+ */
161
+ export function compareQuantities(a: QuantityValue, b: QuantityValue): -1 | 0 | 1 | undefined {
162
+ if (a.calendar && b.calendar) {
163
+ const familyA = calendarFamily(a.unit)
164
+ const familyB = calendarFamily(b.unit)
165
+ if (!(familyA && familyB) || familyA.family !== familyB.family) {
166
+ return undefined
167
+ }
168
+ return a.value.multiply(familyA.factor).compare(b.value.multiply(familyB.factor))
169
+ }
170
+ if (a.calendar || b.calendar) {
171
+ const [calendar, ucum] = a.calendar ? [a, b] : [b, a]
172
+ const exactTwin = CALENDAR_TO_UCUM_EXACT[normalizeCalendarUnit(calendar.unit)]
173
+ if (exactTwin === undefined) {
174
+ return undefined
175
+ }
176
+ const comparison = compareUcum({ ...calendar, unit: exactTwin, calendar: false }, ucum)
177
+ return comparison === undefined ? undefined : a.calendar ? comparison : (-comparison as -1 | 0 | 1)
178
+ }
179
+ return compareUcum(a, b)
180
+ }
181
+
182
+ function compareUcum(a: QuantityValue, b: QuantityValue): -1 | 0 | 1 | undefined {
183
+ if (a.unit === b.unit) {
184
+ return a.value.compare(b.value)
185
+ }
186
+ const canonicalA = canonicalizeUnit(a.unit)
187
+ const canonicalB = canonicalizeUnit(b.unit)
188
+ if (!(canonicalA && canonicalB && sameDimensions(canonicalA, canonicalB))) {
189
+ return undefined
190
+ }
191
+ return a.value.multiply(canonicalA.factor).compare(b.value.multiply(canonicalB.factor))
192
+ }
193
+
194
+ /**
195
+ * Equivalence: calendar durations match their UCUM twins (`1 year ~ 1 'a'`), and
196
+ * values compare after conversion, rounded to the least precise operand
197
+ * (`4 'g' ~ 4040 'mg'` is true: 4.04 g rounds to 4 at scale 0).
198
+ */
199
+ export function quantitiesEquivalent(a: QuantityValue, b: QuantityValue): boolean {
200
+ const ucumA = asUcum(a)
201
+ const ucumB = asUcum(b)
202
+ const scale = Math.max(Math.min(a.value.scale, b.value.scale), 0)
203
+ const converted = convertQuantity(ucumB, ucumA.unit)
204
+ if (converted !== undefined) {
205
+ return ucumA.value.round(scale).equals(converted.value.round(scale))
206
+ }
207
+ return compareUcum({ ...ucumA, value: ucumA.value.round(scale) }, { ...ucumB, value: ucumB.value.round(scale) }) === 0
208
+ }
209
+
210
+ /** A calendar duration as its loose UCUM twin (`1 year` → `1 'a'`); UCUM passes through. */
211
+ export function calendarToUcumLoose(quantity: QuantityValue): QuantityValue {
212
+ return asUcum(quantity)
213
+ }
214
+
215
+ function asUcum(quantity: QuantityValue): QuantityValue {
216
+ if (!quantity.calendar) {
217
+ return quantity
218
+ }
219
+ const twin = CALENDAR_TO_UCUM_LOOSE[normalizeCalendarUnit(quantity.unit)]
220
+ /* v8 ignore next 3 -- every calendar word has a loose twin */
221
+ if (twin === undefined) {
222
+ return quantity
223
+ }
224
+ return { ...quantity, unit: twin, calendar: false }
225
+ }
226
+
227
+ /** True for `year`/`days`/... — never for UCUM codes like `wk` or `mo`. */
228
+ function isCalendarWord(unit: string): boolean {
229
+ const singular = normalizeCalendarUnit(unit)
230
+ return MONTH_FAMILY[singular] !== undefined || SECOND_FAMILY[singular] !== undefined
231
+ }
232
+
233
+ /**
234
+ * Convert to another unit. UCUM↔UCUM converts through base dimensions; calendar
235
+ * words convert within their family (`12 months` → `1 year`, `1 week` → `7 days`),
236
+ * and week-and-below calendar words cross into UCUM through their exact twins
237
+ * (`1 'wk'` → `7 days`). Year and month never convert to definite UCUM durations.
238
+ * Undefined when no exact conversion exists.
239
+ */
240
+ export function convertQuantity(quantity: QuantityValue, targetUnit: string): QuantityValue | undefined {
241
+ if (quantity.unit === targetUnit && !quantity.calendar === !isCalendarWord(targetUnit)) {
242
+ return quantity
243
+ }
244
+ const targetIsCalendar = isCalendarWord(targetUnit)
245
+ if (quantity.calendar || targetIsCalendar) {
246
+ return convertCalendar(quantity, targetUnit, targetIsCalendar)
247
+ }
248
+ const source = canonicalizeUnit(quantity.unit)
249
+ const target = canonicalizeUnit(targetUnit)
250
+ if (!(source && target && sameDimensions(source, target))) {
251
+ return undefined
252
+ }
253
+ const converted = quantity.value.multiply(source.factor).divide(target.factor)
254
+ return converted === undefined ? undefined : { value: converted, unit: targetUnit, calendar: false }
255
+ }
256
+
257
+ function convertCalendar(
258
+ quantity: QuantityValue,
259
+ targetUnit: string,
260
+ targetIsCalendar: boolean
261
+ ): QuantityValue | undefined {
262
+ const sourceFamily = quantity.calendar ? calendarFamily(quantity.unit) : undefined
263
+ const targetFamily = targetIsCalendar ? calendarFamily(normalizeCalendarUnit(targetUnit)) : undefined
264
+ if (quantity.calendar && targetIsCalendar) {
265
+ if (!(sourceFamily && targetFamily) || sourceFamily.family !== targetFamily.family) {
266
+ return undefined
267
+ }
268
+ const converted = quantity.value.multiply(sourceFamily.factor).divide(targetFamily.factor)
269
+ return converted === undefined ? undefined : { value: converted, unit: targetUnit, calendar: true }
270
+ }
271
+ if (quantity.calendar) {
272
+ // Calendar → UCUM: only week-and-below are definite; year/month stay calendar-only.
273
+ if (sourceFamily?.family !== 'second') {
274
+ return undefined
275
+ }
276
+ const twin = CALENDAR_TO_UCUM_EXACT[normalizeCalendarUnit(quantity.unit)] as string
277
+ return convertQuantity({ value: quantity.value, unit: twin, calendar: false }, targetUnit)
278
+ }
279
+ // UCUM → calendar word: cross through seconds; year/month targets are indefinite.
280
+ if (targetFamily?.family !== 'second') {
281
+ return undefined
282
+ }
283
+ const source = canonicalizeUnit(quantity.unit)
284
+ const secondsPerTarget = canonicalizeUnit(
285
+ CALENDAR_TO_UCUM_EXACT[normalizeCalendarUnit(targetUnit)] as string
286
+ ) as NonNullable<ReturnType<typeof canonicalizeUnit>>
287
+ if (!(source && sameDimensions(source, secondsPerTarget))) {
288
+ return undefined
289
+ }
290
+ const converted = quantity.value.multiply(source.factor).divide(secondsPerTarget.factor)
291
+ return converted === undefined ? undefined : { value: converted, unit: targetUnit, calendar: true }
292
+ }
293
+
294
+ /**
295
+ * Bring two quantities to a shared unit for + and -; the spec picks the more
296
+ * granular (smaller) unit of the two. Undefined when they cannot be converted.
297
+ */
298
+ export function alignQuantities(
299
+ a: QuantityValue,
300
+ b: QuantityValue
301
+ ): { left: Decimal; right: Decimal; unit: string; calendar: boolean } | undefined {
302
+ if (a.unit === b.unit && a.calendar === b.calendar) {
303
+ return { left: a.value, right: b.value, unit: a.unit, calendar: a.calendar }
304
+ }
305
+ if (a.calendar && b.calendar) {
306
+ const familyA = calendarFamily(a.unit)
307
+ const familyB = calendarFamily(b.unit)
308
+ if (!(familyA && familyB) || familyA.family !== familyB.family) {
309
+ return undefined
310
+ }
311
+ const smaller = familyA.factor.compare(familyB.factor) <= 0 ? a : b
312
+ const factor = calendarFamily(smaller.unit)?.factor as Decimal
313
+ return {
314
+ left: a.value.multiply(familyA.factor).divide(factor) as Decimal,
315
+ right: b.value.multiply(familyB.factor).divide(factor) as Decimal,
316
+ unit: normalizeCalendarUnit(smaller.unit),
317
+ calendar: true,
318
+ }
319
+ }
320
+ if (a.calendar || b.calendar) {
321
+ const exactA = a.calendar ? CALENDAR_TO_UCUM_EXACT[normalizeCalendarUnit(a.unit)] : a.unit
322
+ const exactB = b.calendar ? CALENDAR_TO_UCUM_EXACT[normalizeCalendarUnit(b.unit)] : b.unit
323
+ if (exactA === undefined || exactB === undefined) {
324
+ return undefined
325
+ }
326
+ return alignQuantities(
327
+ { value: a.value, unit: exactA, calendar: false },
328
+ { value: b.value, unit: exactB, calendar: false }
329
+ )
330
+ }
331
+ const canonicalA = canonicalizeUnit(a.unit)
332
+ const canonicalB = canonicalizeUnit(b.unit)
333
+ if (!(canonicalA && canonicalB && sameDimensions(canonicalA, canonicalB))) {
334
+ return undefined
335
+ }
336
+ const targetUnit = canonicalA.factor.compare(canonicalB.factor) <= 0 ? a.unit : b.unit
337
+ const left = convertQuantity(a, targetUnit)
338
+ const right = convertQuantity(b, targetUnit)
339
+ /* v8 ignore next 3 -- both conversions share the dimensions checked above */
340
+ if (!(left && right)) {
341
+ return undefined
342
+ }
343
+ return { left: left.value, right: right.value, unit: targetUnit, calendar: false }
344
+ }
345
+
346
+ /** Compose the result unit of quantity multiplication or division. */
347
+ export function composeUnits(operator: '*' | '/', a: string, b: string): string {
348
+ if (operator === '*') {
349
+ if (a === '1') {
350
+ return b
351
+ }
352
+ return b === '1' ? a : `${a}.${b}`
353
+ }
354
+ if (a === b) {
355
+ return '1'
356
+ }
357
+ return b === '1' ? a : `${a}/${b}`
358
+ }
@@ -0,0 +1,231 @@
1
+ import { FhirPathRuntimeError } from '../errors.ts'
2
+ import { daysInMonth, GREGORIAN_UTC_YEAR_OFFSET, Temporal, type TemporalPrecision } from './datetime.ts'
3
+ import { normalizeCalendarUnit } from './quantity.ts'
4
+ import type { QuantityValue } from './typed-value.ts'
5
+
6
+ /** Position of each precision on the component ladder used for duration conversion. */
7
+ const COMPONENT_LEVELS: Readonly<Record<string, number>> = {
8
+ year: 0,
9
+ month: 1,
10
+ day: 2,
11
+ hour: 3,
12
+ minute: 4,
13
+ second: 5,
14
+ millisecond: 6,
15
+ }
16
+
17
+ /** Truncated-division factors for converting a duration one level up (day→month uses 30). */
18
+ const UP_CONVERSION_FACTORS = [12, 30, 24, 60, 60, 1000]
19
+
20
+ const UCUM_TIME_UNITS: Readonly<Record<string, { level: number; multiplier: number }>> = {
21
+ wk: { level: 2, multiplier: 7 },
22
+ d: { level: 2, multiplier: 1 },
23
+ h: { level: 3, multiplier: 1 },
24
+ min: { level: 4, multiplier: 1 },
25
+ s: { level: 5, multiplier: 1 },
26
+ ms: { level: 6, multiplier: 1 },
27
+ }
28
+
29
+ const MS_PER_LEVEL: Readonly<Record<number, number>> = {
30
+ 2: 86400000,
31
+ 3: 3600000,
32
+ 4: 60000,
33
+ 5: 1000,
34
+ 6: 1,
35
+ }
36
+
37
+ interface Duration {
38
+ /** Component level the duration applies to (0=year ... 6=millisecond). */
39
+ level: number
40
+ /** Amount in that component's unit; may be fractional only at the seconds level. */
41
+ amount: number
42
+ }
43
+
44
+ function resolveDuration(quantity: QuantityValue): Duration {
45
+ // The decimal portion of units above seconds is ignored (spec §6.6.7, official
46
+ // testPlusDate2: 7.7 days adds 7) — truncate before any week/day expansion.
47
+ const value = quantity.value.toNumber()
48
+ const wholeAmount = Math.trunc(value)
49
+ const unit = normalizeCalendarUnit(quantity.unit)
50
+ if (COMPONENT_LEVELS[unit] !== undefined || unit === 'week') {
51
+ if (unit === 'week') {
52
+ return { level: 2, amount: wholeAmount * 7 }
53
+ }
54
+ const level = COMPONENT_LEVELS[unit] as number
55
+ return { level, amount: level >= 5 ? value : wholeAmount }
56
+ }
57
+ if (quantity.calendar) {
58
+ /* v8 ignore next 2 -- the lexer only produces the calendar words above */
59
+ throw new FhirPathRuntimeError(`Unknown calendar duration unit '${quantity.unit}'`)
60
+ }
61
+ const ucum = UCUM_TIME_UNITS[quantity.unit]
62
+ if (!ucum) {
63
+ // The spec reserves UCUM 'a' and 'mo' (definite 365/30-day durations) as errors
64
+ // for calendar arithmetic; calendar words must be used instead.
65
+ throw new FhirPathRuntimeError(
66
+ `Cannot use UCUM unit '${quantity.unit}' for date/time arithmetic; use a calendar duration keyword`
67
+ )
68
+ }
69
+ const amount = ucum.level >= 5 ? value * ucum.multiplier : wholeAmount * ucum.multiplier
70
+ return { level: ucum.level, amount }
71
+ }
72
+
73
+ /** Week is not a real component; it resolves to days before this table is consulted. */
74
+ function precisionLevel(precision: TemporalPrecision): number {
75
+ return COMPONENT_LEVELS[precision] as number
76
+ }
77
+
78
+ /**
79
+ * `temporal ± quantity` (spec §6.6.7): the duration converts down to the value's
80
+ * precision with truncation (`@2014 + 23 months` adds 1 year), calendar components
81
+ * roll over, and day-of-month clamps (`@2020-01-31 + 1 month` → `@2020-02-29`).
82
+ * Undefined when the resulting year leaves 0001–9999.
83
+ */
84
+ export function addDuration(temporal: Temporal, quantity: QuantityValue, sign: 1 | -1): Temporal | undefined {
85
+ const duration = resolveDuration(quantity)
86
+ // Times have no year/month/day components to add to.
87
+ const dayLevel = 2
88
+ if (temporal.kind === 'time' && duration.level <= dayLevel) {
89
+ throw new FhirPathRuntimeError(`Cannot add a quantity of '${quantity.unit}' to a Time value`)
90
+ }
91
+ let level = duration.level
92
+ let amount = duration.amount * sign
93
+ let targetLevel = Math.min(precisionLevel(temporal.precision), temporal.kind === 'date' ? 2 : 6)
94
+ // Fractional seconds add as milliseconds (R5); millisecond-level additions
95
+ // extend a second-precision value to milliseconds instead of truncating.
96
+ if (level === 5 && !Number.isInteger(amount)) {
97
+ level = 6
98
+ amount = Math.round(amount * 1000)
99
+ }
100
+ if (level === 6 && targetLevel === 5) {
101
+ targetLevel = 6
102
+ }
103
+ while (level > targetLevel) {
104
+ if (level === 2 && targetLevel === 0) {
105
+ // Days convert straight to years; going through 30-day months undercounts.
106
+ amount = Math.trunc(amount / 365)
107
+ level = 0
108
+ break
109
+ }
110
+ amount = Math.trunc(amount / (UP_CONVERSION_FACTORS[level - 1] as number))
111
+ level -= 1
112
+ }
113
+ if (amount === 0) {
114
+ return temporal
115
+ }
116
+ if (level <= 1) {
117
+ return addCalendarComponent(temporal, level, amount)
118
+ }
119
+ return addMilliseconds(temporal, amount * (MS_PER_LEVEL[level] as number))
120
+ }
121
+
122
+ function addCalendarComponent(temporal: Temporal, level: number, amount: number): Temporal | undefined {
123
+ let year = temporal.year as number
124
+ let month = temporal.month
125
+ if (level === 0) {
126
+ year += amount
127
+ } else {
128
+ const zeroBased = (month ?? 1) - 1 + amount
129
+ year += Math.floor(zeroBased / 12)
130
+ month = (((zeroBased % 12) + 12) % 12) + 1
131
+ }
132
+ if (year < 1 || year > 9999) {
133
+ return undefined
134
+ }
135
+ let day = temporal.day
136
+ if (day !== undefined && month !== undefined) {
137
+ day = Math.min(day, daysInMonth(year, month))
138
+ }
139
+ return rebuild(temporal, { year, month, day })
140
+ }
141
+
142
+ function addMilliseconds(temporal: Temporal, deltaMs: number): Temporal | undefined {
143
+ if (temporal.kind === 'time') {
144
+ const baseMs =
145
+ (temporal.hour ?? 0) * 3600000 +
146
+ (temporal.minute ?? 0) * 60000 +
147
+ Math.round(((temporal.second ?? 0) + fractionOf(temporal)) * 1000)
148
+ // Time arithmetic wraps around the day.
149
+ const dayMs = 86400000
150
+ const total = (((baseMs + deltaMs) % dayMs) + dayMs) % dayMs
151
+ return rebuild(temporal, componentsFromDayMs(total, temporal))
152
+ }
153
+ const shifted = new Date(
154
+ Date.UTC(
155
+ (temporal.year as number) + GREGORIAN_UTC_YEAR_OFFSET,
156
+ (temporal.month ?? 1) - 1,
157
+ temporal.day ?? 1,
158
+ temporal.hour ?? 0,
159
+ temporal.minute ?? 0,
160
+ 0,
161
+ Math.round(((temporal.second ?? 0) + fractionOf(temporal)) * 1000) + deltaMs
162
+ )
163
+ )
164
+ const year = shifted.getUTCFullYear() - 400
165
+ if (year < 1 || year > 9999) {
166
+ return undefined
167
+ }
168
+ return rebuild(temporal, {
169
+ year,
170
+ month: temporal.month === undefined ? undefined : shifted.getUTCMonth() + 1,
171
+ day: temporal.day === undefined ? undefined : shifted.getUTCDate(),
172
+ hour: temporal.hour === undefined ? undefined : shifted.getUTCHours(),
173
+ minute: temporal.minute === undefined ? undefined : shifted.getUTCMinutes(),
174
+ second: temporal.second === undefined ? undefined : shifted.getUTCSeconds(),
175
+ fraction:
176
+ temporal.fraction === undefined && shifted.getUTCMilliseconds() === 0
177
+ ? undefined
178
+ : String(shifted.getUTCMilliseconds()).padStart(3, '0'),
179
+ })
180
+ }
181
+
182
+ function componentsFromDayMs(
183
+ totalMs: number,
184
+ temporal: Temporal
185
+ ): { hour: number; minute?: number | undefined; second?: number | undefined; fraction?: string | undefined } {
186
+ const hour = Math.floor(totalMs / 3600000)
187
+ const minute = Math.floor((totalMs % 3600000) / 60000)
188
+ const second = Math.floor((totalMs % 60000) / 1000)
189
+ const ms = totalMs % 1000
190
+ return {
191
+ hour,
192
+ minute: temporal.minute === undefined ? undefined : minute,
193
+ second: temporal.second === undefined ? undefined : second,
194
+ fraction: temporal.fraction === undefined && ms === 0 ? undefined : String(ms).padStart(3, '0'),
195
+ }
196
+ }
197
+
198
+ function fractionOf(temporal: Temporal): number {
199
+ return temporal.fraction === undefined ? 0 : Number.parseFloat(`0.${temporal.fraction}`)
200
+ }
201
+
202
+ function rebuild(
203
+ temporal: Temporal,
204
+ overrides: {
205
+ year?: number | undefined
206
+ month?: number | undefined
207
+ day?: number | undefined
208
+ hour?: number | undefined
209
+ minute?: number | undefined
210
+ second?: number | undefined
211
+ fraction?: string | undefined
212
+ }
213
+ ): Temporal {
214
+ const rebuilt = Temporal.fromFields(temporal.kind, {
215
+ year: 'year' in overrides ? overrides.year : temporal.year,
216
+ month: 'month' in overrides ? overrides.month : temporal.month,
217
+ day: 'day' in overrides ? overrides.day : temporal.day,
218
+ hour: 'hour' in overrides ? overrides.hour : temporal.hour,
219
+ minute: 'minute' in overrides ? overrides.minute : temporal.minute,
220
+ second: 'second' in overrides ? overrides.second : temporal.second,
221
+ fraction: 'fraction' in overrides ? overrides.fraction : temporal.fraction,
222
+ timezoneOffsetMinutes: temporal.timezoneOffsetMinutes,
223
+ })
224
+ /* v8 ignore next 3 -- arithmetic always produces in-range components */
225
+ if (!rebuilt) {
226
+ throw new FhirPathRuntimeError('Date/time arithmetic produced an invalid value')
227
+ }
228
+ return rebuilt
229
+ }
230
+
231
+ export type { Duration }
@@ -0,0 +1,65 @@
1
+ import { GREGORIAN_UTC_YEAR_OFFSET, type Temporal } from './datetime.ts'
2
+
3
+ /**
4
+ * Precision levels for comparison. Seconds and milliseconds are one level: the spec
5
+ * treats the second field as a decimal, so `@T10:00:00` and `@T10:00:00.0` compare
6
+ * as equal values rather than as a precision mismatch.
7
+ */
8
+ const LEVELS = ['year', 'month', 'day', 'hour', 'minute', 'second'] as const
9
+ type Level = (typeof LEVELS)[number]
10
+
11
+ export type TemporalComparison = -1 | 0 | 1 | 'differentPrecision' | 'incompatible'
12
+
13
+ /**
14
+ * Compare two temporal values per spec §6.1/§6.4: equal at the shared precision but
15
+ * with different precisions → 'differentPrecision' (empty for `=` and `<`); a time
16
+ * never compares with a date/dateTime. Values without a timezone are treated as UTC.
17
+ */
18
+ export function compareTemporal(a: Temporal, b: Temporal): TemporalComparison {
19
+ if ((a.kind === 'time') !== (b.kind === 'time')) {
20
+ return 'incompatible'
21
+ }
22
+ const levelA = comparisonLevel(a)
23
+ const levelB = comparisonLevel(b)
24
+ const shared = Math.min(levelA, levelB)
25
+ // At time precision, a value with a timezone and one without cannot be ordered (spec §6.1).
26
+ if (shared >= 3 && (a.timezoneOffsetMinutes === undefined) !== (b.timezoneOffsetMinutes === undefined)) {
27
+ return 'differentPrecision'
28
+ }
29
+ const rankA = rank(a, shared)
30
+ const rankB = rank(b, shared)
31
+ if (rankA < rankB) {
32
+ return -1
33
+ }
34
+ if (rankA > rankB) {
35
+ return 1
36
+ }
37
+ return levelA === levelB ? 0 : 'differentPrecision'
38
+ }
39
+
40
+ function comparisonLevel(value: Temporal): number {
41
+ const precision = value.precision === 'millisecond' ? 'second' : value.precision
42
+ return LEVELS.indexOf(precision as Level)
43
+ }
44
+
45
+ /**
46
+ * Milliseconds on a common axis, truncated to `level`. Years are shifted by +400 to
47
+ * dodge Date.UTC's 0-99 → 19xx mapping; the Gregorian calendar repeats exactly every
48
+ * 400 years, so ordering and equality are unaffected.
49
+ */
50
+ function rank(value: Temporal, level: number): number {
51
+ const year = (value.year ?? 1970) + GREGORIAN_UTC_YEAR_OFFSET
52
+ const month = level >= 1 ? (value.month ?? 1) : 1
53
+ const day = level >= 2 ? (value.day ?? 1) : 1
54
+ const hour = level >= 3 ? (value.hour ?? 0) : 0
55
+ let minute = level >= 4 ? (value.minute ?? 0) : 0
56
+ let milliseconds = 0
57
+ if (level >= 5) {
58
+ const fraction = value.fraction === undefined ? 0 : Number.parseFloat(`0.${value.fraction}`)
59
+ milliseconds = Math.round(((value.second ?? 0) + fraction) * 1000)
60
+ }
61
+ if (level >= 3 && value.timezoneOffsetMinutes !== undefined) {
62
+ minute -= value.timezoneOffsetMinutes
63
+ }
64
+ return Date.UTC(year, month - 1, day, hour, minute, 0, milliseconds)
65
+ }