@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.
- package/README.md +716 -238
- package/dist/index.d.ts +226 -120
- package/dist/index.js +11552 -5580
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +939 -1204
- package/src/completion-provider.ts +209 -191
- package/src/complex-types/quantity-value.ts +410 -0
- package/src/complex-types/temporal.ts +1776 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +506 -468
- package/src/lexer.ts +192 -211
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +99 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +744 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +132 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/extension-function.ts +84 -0
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +7 -9
- package/src/operations/greater-or-equal-operator.ts +7 -9
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +193 -8
- package/src/operations/implies-operator.ts +2 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +43 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +70 -0
- package/src/operations/is-operator.ts +176 -13
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +8 -9
- package/src/operations/less-or-equal-operator.ts +7 -9
- package/src/operations/less-than.ts +8 -13
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +76 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +8 -2
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +10 -3
- package/src/operations/ofType-function.ts +43 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +125 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +78 -15
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +262 -503
- package/src/registry.ts +53 -42
- package/src/types.ts +129 -17
- package/src/utils/decimal.ts +76 -0
- package/src/utils/pprint.ts +151 -0
- package/src/quantity-value.ts +0 -198
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { ucum } from '@atomic-ehr/ucum';
|
|
2
|
+
import type { Quantity } from '@atomic-ehr/ucum';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrapper for FHIRPath quantity values with UCUM integration
|
|
6
|
+
*/
|
|
7
|
+
export interface QuantityValue {
|
|
8
|
+
value: number;
|
|
9
|
+
unit: string;
|
|
10
|
+
_ucumQuantity?: Quantity; // Lazy-initialized
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calendar duration units used in FHIRPath
|
|
15
|
+
* These are NOT UCUM units and should not be converted
|
|
16
|
+
*/
|
|
17
|
+
export const CALENDAR_DURATION_UNITS = new Set([
|
|
18
|
+
'year', 'years',
|
|
19
|
+
'month', 'months',
|
|
20
|
+
'week', 'weeks',
|
|
21
|
+
'day', 'days',
|
|
22
|
+
'hour', 'hours',
|
|
23
|
+
'minute', 'minutes',
|
|
24
|
+
'second', 'seconds',
|
|
25
|
+
'millisecond', 'milliseconds'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calendar to UCUM duration mappings
|
|
30
|
+
* Maps FHIRPath calendar duration units to their UCUM equivalents
|
|
31
|
+
*/
|
|
32
|
+
export const CALENDAR_TO_UCUM: Record<string, string> = {
|
|
33
|
+
'year': 'a',
|
|
34
|
+
'years': 'a',
|
|
35
|
+
'month': 'mo',
|
|
36
|
+
'months': 'mo',
|
|
37
|
+
'week': 'wk',
|
|
38
|
+
'weeks': 'wk',
|
|
39
|
+
'day': 'd',
|
|
40
|
+
'days': 'd',
|
|
41
|
+
'hour': 'h',
|
|
42
|
+
'hours': 'h',
|
|
43
|
+
'minute': 'min',
|
|
44
|
+
'minutes': 'min',
|
|
45
|
+
'second': 's',
|
|
46
|
+
'seconds': 's',
|
|
47
|
+
'millisecond': 'ms',
|
|
48
|
+
'milliseconds': 'ms'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a quantity value
|
|
53
|
+
*/
|
|
54
|
+
export function createQuantity(value: number, unit: string): QuantityValue {
|
|
55
|
+
return {
|
|
56
|
+
value,
|
|
57
|
+
unit
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get or create the UCUM quantity for a QuantityValue
|
|
63
|
+
*/
|
|
64
|
+
export function getUcumQuantity(quantity: QuantityValue): Quantity | null {
|
|
65
|
+
// Calendar duration units are not UCUM units
|
|
66
|
+
if (CALENDAR_DURATION_UNITS.has(quantity.unit)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!quantity._ucumQuantity) {
|
|
71
|
+
try {
|
|
72
|
+
quantity._ucumQuantity = ucum.quantity(quantity.value, quantity.unit);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Invalid unit - return null
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return quantity._ucumQuantity || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if a quantity has a valid unit
|
|
83
|
+
* Valid units are either UCUM units or calendar duration units
|
|
84
|
+
*/
|
|
85
|
+
export function isValidQuantity(quantity: QuantityValue): boolean {
|
|
86
|
+
// Calendar duration units are valid FHIRPath quantities
|
|
87
|
+
if (CALENDAR_DURATION_UNITS.has(quantity.unit)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Otherwise check if it's a valid UCUM unit
|
|
91
|
+
return getUcumQuantity(quantity) !== null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Add two quantities
|
|
96
|
+
*/
|
|
97
|
+
export function addQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
|
|
98
|
+
// Special case: adding calendar durations with the same unit
|
|
99
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) && left.unit === right.unit) {
|
|
100
|
+
return {
|
|
101
|
+
value: left.value + right.value,
|
|
102
|
+
unit: left.unit
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Different calendar units cannot be added
|
|
107
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const leftUcum = getUcumQuantity(left);
|
|
112
|
+
const rightUcum = getUcumQuantity(right);
|
|
113
|
+
|
|
114
|
+
if (!leftUcum || !rightUcum) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = ucum.add(leftUcum, rightUcum);
|
|
120
|
+
return {
|
|
121
|
+
value: result.value,
|
|
122
|
+
unit: result.unit
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Incompatible dimensions
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Subtract two quantities
|
|
132
|
+
*/
|
|
133
|
+
export function subtractQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
|
|
134
|
+
// Special case: subtracting calendar durations with the same unit
|
|
135
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) && left.unit === right.unit) {
|
|
136
|
+
return {
|
|
137
|
+
value: left.value - right.value,
|
|
138
|
+
unit: left.unit
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Different calendar units cannot be subtracted
|
|
143
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const leftUcum = getUcumQuantity(left);
|
|
148
|
+
const rightUcum = getUcumQuantity(right);
|
|
149
|
+
|
|
150
|
+
if (!leftUcum || !rightUcum) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = ucum.subtract(leftUcum, rightUcum);
|
|
156
|
+
return {
|
|
157
|
+
value: result.value,
|
|
158
|
+
unit: result.unit
|
|
159
|
+
};
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// Incompatible dimensions
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Multiply two quantities
|
|
168
|
+
*/
|
|
169
|
+
export function multiplyQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
|
|
170
|
+
// Special case: multiplying calendar duration by a dimensionless number
|
|
171
|
+
// e.g., 1 year * 2 = 2 years, or 2 * 1 year = 2 years
|
|
172
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) && right.unit === '1') {
|
|
173
|
+
// Calendar duration * number
|
|
174
|
+
return {
|
|
175
|
+
value: left.value * right.value,
|
|
176
|
+
unit: left.unit
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (CALENDAR_DURATION_UNITS.has(right.unit) && left.unit === '1') {
|
|
180
|
+
// Number * calendar duration
|
|
181
|
+
return {
|
|
182
|
+
value: left.value * right.value,
|
|
183
|
+
unit: right.unit
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if both are calendar duration units - this is not allowed
|
|
188
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) && CALENDAR_DURATION_UNITS.has(right.unit)) {
|
|
189
|
+
// Calendar duration units cannot be multiplied together - the result would be meaningless
|
|
190
|
+
throw new Error(`Cannot multiply calendar duration units: ${left.unit} * ${right.unit}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Mixed calendar and non-calendar units are not allowed
|
|
194
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
|
|
195
|
+
throw new Error(`Cannot multiply calendar duration with non-calendar units: ${left.unit} * ${right.unit}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const leftUcum = getUcumQuantity(left);
|
|
199
|
+
const rightUcum = getUcumQuantity(right);
|
|
200
|
+
|
|
201
|
+
if (!leftUcum || !rightUcum) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const result = ucum.multiply(leftUcum, rightUcum);
|
|
207
|
+
return {
|
|
208
|
+
value: result.value,
|
|
209
|
+
unit: result.unit
|
|
210
|
+
};
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Divide two quantities
|
|
218
|
+
*/
|
|
219
|
+
export function divideQuantities(left: QuantityValue, right: QuantityValue): QuantityValue | null {
|
|
220
|
+
// Special case: dividing calendar duration by a dimensionless number
|
|
221
|
+
// e.g., 10 months / 2 = 5 months
|
|
222
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) && right.unit === '1') {
|
|
223
|
+
// Calendar duration / number
|
|
224
|
+
return {
|
|
225
|
+
value: left.value / right.value,
|
|
226
|
+
unit: left.unit
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Division of number by calendar duration is not allowed
|
|
231
|
+
// Division of two calendar durations is not allowed
|
|
232
|
+
if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
|
|
233
|
+
// Calendar duration units cannot be divided except by dimensionless numbers
|
|
234
|
+
throw new Error(`Cannot divide calendar duration units: ${left.unit} / ${right.unit}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const leftUcum = getUcumQuantity(left);
|
|
238
|
+
const rightUcum = getUcumQuantity(right);
|
|
239
|
+
|
|
240
|
+
if (!leftUcum || !rightUcum) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const result = ucum.divide(leftUcum, rightUcum);
|
|
246
|
+
return {
|
|
247
|
+
value: result.value,
|
|
248
|
+
unit: result.unit
|
|
249
|
+
};
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compare two quantities
|
|
257
|
+
* Returns -1 if left < right, 0 if equal, 1 if left > right, null if incomparable
|
|
258
|
+
*/
|
|
259
|
+
export function compareQuantities(left: QuantityValue, right: QuantityValue): number | null {
|
|
260
|
+
// Handle calendar to UCUM comparisons
|
|
261
|
+
const leftIsCalendar = CALENDAR_DURATION_UNITS.has(left.unit);
|
|
262
|
+
const rightIsCalendar = CALENDAR_DURATION_UNITS.has(right.unit);
|
|
263
|
+
|
|
264
|
+
// Both calendar units - only compare if they're compatible units
|
|
265
|
+
if (leftIsCalendar && rightIsCalendar) {
|
|
266
|
+
// Only allow conversion between specific compatible units
|
|
267
|
+
const areCompatible = (unit1: string, unit2: string): boolean => {
|
|
268
|
+
// Normalize to singular form
|
|
269
|
+
const normalize = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
|
|
270
|
+
const n1 = normalize(unit1);
|
|
271
|
+
const n2 = normalize(unit2);
|
|
272
|
+
|
|
273
|
+
// Same unit (singular/plural)
|
|
274
|
+
if (n1 === n2) return true;
|
|
275
|
+
|
|
276
|
+
// Week <-> days conversion
|
|
277
|
+
if ((n1 === 'week' && n2 === 'day') || (n1 === 'day' && n2 === 'week')) return true;
|
|
278
|
+
|
|
279
|
+
// No other conversions between calendar units
|
|
280
|
+
return false;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (!areCompatible(left.unit, right.unit)) {
|
|
284
|
+
// Different calendar units that aren't compatible
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle week/day conversion
|
|
289
|
+
const normalizeToSingular = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
|
|
290
|
+
const leftNorm = normalizeToSingular(left.unit);
|
|
291
|
+
const rightNorm = normalizeToSingular(right.unit);
|
|
292
|
+
|
|
293
|
+
if (leftNorm === rightNorm) {
|
|
294
|
+
// Same unit, just compare values
|
|
295
|
+
if (left.value < right.value) return -1;
|
|
296
|
+
if (left.value > right.value) return 1;
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Week <-> day conversion
|
|
301
|
+
if ((leftNorm === 'week' && rightNorm === 'day') ||
|
|
302
|
+
(leftNorm === 'day' && rightNorm === 'week')) {
|
|
303
|
+
const leftInDays = leftNorm === 'week' ? left.value * 7 : left.value;
|
|
304
|
+
const rightInDays = rightNorm === 'week' ? right.value * 7 : right.value;
|
|
305
|
+
|
|
306
|
+
if (leftInDays < rightInDays) return -1;
|
|
307
|
+
if (leftInDays > rightInDays) return 1;
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Shouldn't reach here
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Calendar to UCUM comparison
|
|
316
|
+
if (leftIsCalendar && !rightIsCalendar) {
|
|
317
|
+
// Check for week/day special case
|
|
318
|
+
if ((left.unit === 'days' || left.unit === 'day') && right.unit === 'wk') {
|
|
319
|
+
// Convert days to weeks
|
|
320
|
+
const leftInWeeks = left.value / 7;
|
|
321
|
+
if (leftInWeeks < right.value) return -1;
|
|
322
|
+
if (leftInWeeks > right.value) return 1;
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
if ((left.unit === 'weeks' || left.unit === 'week') && right.unit === 'd') {
|
|
326
|
+
// Convert weeks to days
|
|
327
|
+
const leftInDays = left.value * 7;
|
|
328
|
+
if (leftInDays < right.value) return -1;
|
|
329
|
+
if (leftInDays > right.value) return 1;
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Direct mapping check
|
|
334
|
+
const leftUcumUnit = CALENDAR_TO_UCUM[left.unit];
|
|
335
|
+
if (leftUcumUnit === right.unit) {
|
|
336
|
+
// Direct mapping, compare values
|
|
337
|
+
if (left.value < right.value) return -1;
|
|
338
|
+
if (left.value > right.value) return 1;
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
// No mapping, incomparable
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// UCUM to calendar comparison
|
|
346
|
+
if (!leftIsCalendar && rightIsCalendar) {
|
|
347
|
+
// Check for week/day special case
|
|
348
|
+
if (left.unit === 'wk' && (right.unit === 'days' || right.unit === 'day')) {
|
|
349
|
+
// Convert weeks to days
|
|
350
|
+
const leftInDays = left.value * 7;
|
|
351
|
+
if (leftInDays < right.value) return -1;
|
|
352
|
+
if (leftInDays > right.value) return 1;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
if (left.unit === 'd' && (right.unit === 'weeks' || right.unit === 'week')) {
|
|
356
|
+
// Convert days to weeks
|
|
357
|
+
const leftInWeeks = left.value / 7;
|
|
358
|
+
if (leftInWeeks < right.value) return -1;
|
|
359
|
+
if (leftInWeeks > right.value) return 1;
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Direct mapping check
|
|
364
|
+
const rightUcumUnit = CALENDAR_TO_UCUM[right.unit];
|
|
365
|
+
if (left.unit === rightUcumUnit) {
|
|
366
|
+
// Direct mapping, compare values
|
|
367
|
+
if (left.value < right.value) return -1;
|
|
368
|
+
if (left.value > right.value) return 1;
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
// No mapping, incomparable
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const leftUcum = getUcumQuantity(left);
|
|
376
|
+
const rightUcum = getUcumQuantity(right);
|
|
377
|
+
|
|
378
|
+
if (!leftUcum || !rightUcum) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
// Try to convert right to left's unit for comparison
|
|
384
|
+
const rightValue = ucum.convert(rightUcum.value, rightUcum.unit, leftUcum.unit);
|
|
385
|
+
if (leftUcum.value < rightValue) {
|
|
386
|
+
return -1;
|
|
387
|
+
} else if (leftUcum.value > rightValue) {
|
|
388
|
+
return 1;
|
|
389
|
+
} else {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
} catch (e) {
|
|
393
|
+
// Incompatible dimensions
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if two quantities are equal
|
|
400
|
+
*/
|
|
401
|
+
export function equalQuantities(left: QuantityValue, right: QuantityValue): boolean {
|
|
402
|
+
return compareQuantities(left, right) === 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Convert quantity to string representation
|
|
407
|
+
*/
|
|
408
|
+
export function quantityToString(quantity: QuantityValue): string {
|
|
409
|
+
return `${quantity.value} '${quantity.unit}'`;
|
|
410
|
+
}
|