@callis/ucum 0.0.1 → 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1000 @@
1
+ import { UCUM_ESSENCE_XML } from "./essence/ucum-essence-xml.js";
2
+ import { Decimal, MAX_DECIMAL_DIGITS, minimalUnitSystem } from "@callis/core";
3
+ // src/essence/xml-elements.ts
4
+ const NAME = /[A-Za-z_:][-A-Za-z0-9_:.]*/y;
5
+ const ATTRIBUTE = /\s+([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"<]*)"|'([^'<]*)')/y;
6
+ const TAG_END = /\s*(\/?)>/y;
7
+ const PREDEFINED_ENTITIES = Object.freeze({
8
+ amp: "&",
9
+ apos: "'",
10
+ gt: ">",
11
+ lt: "<",
12
+ quot: "\""
13
+ });
14
+ /**
15
+ * Reads the element tree of a well-formed XML document, as far as the UCUM essence file needs:
16
+ * elements, attributes, comments, processing instructions and CDATA sections. Text is skipped.
17
+ * Document type declarations are rejected, so no external or custom entities are ever expanded.
18
+ *
19
+ * @returns the root element, or `undefined` if the text is not well-formed.
20
+ * @see https://www.w3.org/TR/xml/
21
+ */
22
+ function readXmlElements(text) {
23
+ const stack = [];
24
+ let root;
25
+ let index = 0;
26
+ while (index < text.length) {
27
+ const open = text.indexOf("<", index);
28
+ if (open === -1) {
29
+ if (!isAllowedText(text.slice(index), stack.length)) return void 0;
30
+ break;
31
+ }
32
+ if (!isAllowedText(text.slice(index, open), stack.length)) return void 0;
33
+ if (text.startsWith("<?", open)) {
34
+ const end = text.indexOf("?>", open + 2);
35
+ if (end === -1) return void 0;
36
+ index = end + 2;
37
+ } else if (text.startsWith("<!--", open)) {
38
+ const end = text.indexOf("-->", open + 4);
39
+ if (end === -1) return void 0;
40
+ index = end + 3;
41
+ } else if (text.startsWith("<![CDATA[", open)) {
42
+ const end = text.indexOf("]]>", open + 9);
43
+ if (end === -1 || stack.length === 0) return void 0;
44
+ index = end + 3;
45
+ } else if (text.startsWith("<!", open)) return;
46
+ else if (text.startsWith("</", open)) {
47
+ NAME.lastIndex = open + 2;
48
+ const name = NAME.exec(text)?.[0];
49
+ const element = stack.pop();
50
+ if (name === void 0 || element === void 0 || element.name !== name) return void 0;
51
+ TAG_END.lastIndex = open + 2 + name.length;
52
+ const end = TAG_END.exec(text);
53
+ if (end === null || end[1] === "/") return void 0;
54
+ index = TAG_END.lastIndex;
55
+ const closed = {
56
+ name: element.name,
57
+ attributes: element.attributes,
58
+ children: element.children
59
+ };
60
+ const parent = stack.at(-1);
61
+ if (parent === void 0) root = closed;
62
+ else parent.children.push(closed);
63
+ } else {
64
+ if (root !== void 0) return void 0;
65
+ NAME.lastIndex = open + 1;
66
+ const name = NAME.exec(text)?.[0];
67
+ if (name === void 0) return void 0;
68
+ index = open + 1 + name.length;
69
+ const attributes = /* @__PURE__ */ new Map();
70
+ for (;;) {
71
+ ATTRIBUTE.lastIndex = index;
72
+ const attribute = ATTRIBUTE.exec(text);
73
+ if (attribute === null) break;
74
+ const [, key = "", double, single] = attribute;
75
+ const value = decodeEntities(double ?? single ?? "");
76
+ if (value === void 0 || attributes.has(key)) return void 0;
77
+ attributes.set(key, value);
78
+ index = ATTRIBUTE.lastIndex;
79
+ }
80
+ TAG_END.lastIndex = index;
81
+ const end = TAG_END.exec(text);
82
+ if (end === null) return void 0;
83
+ index = TAG_END.lastIndex;
84
+ if (end[1] === "/") {
85
+ const element = {
86
+ name,
87
+ attributes,
88
+ children: []
89
+ };
90
+ const parent = stack.at(-1);
91
+ if (parent === void 0) root = element;
92
+ else parent.children.push(element);
93
+ } else stack.push({
94
+ name,
95
+ attributes,
96
+ children: []
97
+ });
98
+ }
99
+ }
100
+ return stack.length === 0 ? root : void 0;
101
+ }
102
+ /** Text inside elements is skipped; outside the root element only whitespace may appear. */
103
+ function isAllowedText(text, depth) {
104
+ return depth > 0 || /^[ \t\r\n]*$/.test(text);
105
+ }
106
+ /** Replaces the predefined entities and character references; undefined for anything else. */
107
+ function decodeEntities(value) {
108
+ let invalid = false;
109
+ const decoded = value.replace(/&([^;]*);?/g, (match, reference) => {
110
+ if (!match.endsWith(";")) {
111
+ invalid = true;
112
+ return "";
113
+ }
114
+ if (Object.hasOwn(PREDEFINED_ENTITIES, reference)) return PREDEFINED_ENTITIES[reference] ?? "";
115
+ const code = /^#[0-9]+$/.test(reference) ? Number.parseInt(reference.slice(1), 10) : /^#x[0-9A-Fa-f]+$/.test(reference) ? Number.parseInt(reference.slice(2), 16) : NaN;
116
+ if (!Number.isInteger(code) || code < 1 || code > 1114111 || code >= 55296 && code <= 57343) {
117
+ invalid = true;
118
+ return "";
119
+ }
120
+ return String.fromCodePoint(code);
121
+ });
122
+ return invalid ? void 0 : decoded;
123
+ }
124
+ // src/essence/essence-reader.ts
125
+ /**
126
+ * Reads prefixes, base units and units from the text of `ucum-essence.xml`. Only the case-sensitive
127
+ * `Code` attributes are used. Names, print symbols and the other descriptive content are skipped.
128
+ *
129
+ * @returns the definitions, or `undefined` if the text is not a well-formed essence document.
130
+ * @see https://ucum.org/ucum#section-Character-Set-and-Lexical-Rules
131
+ */
132
+ function readEssence(xml) {
133
+ const root = readXmlElements(xml);
134
+ if (root === void 0 || root.name !== "root") return void 0;
135
+ const prefixes = [];
136
+ const baseUnits = [];
137
+ const units = [];
138
+ for (const element of root.children) {
139
+ const code = element.attributes.get("Code");
140
+ if (element.name === "prefix") {
141
+ const value = child(element, "value")?.attributes.get("value");
142
+ if (code === void 0 || value === void 0) return void 0;
143
+ prefixes.push({
144
+ code,
145
+ value
146
+ });
147
+ } else if (element.name === "base-unit") {
148
+ const dimension = element.attributes.get("dim");
149
+ if (code === void 0 || dimension === void 0) return void 0;
150
+ baseUnits.push({
151
+ code,
152
+ dimension
153
+ });
154
+ } else if (element.name === "unit") {
155
+ const unit = readUnit(element, code);
156
+ if (unit === void 0) return void 0;
157
+ units.push(unit);
158
+ }
159
+ }
160
+ return {
161
+ version: root.attributes.get("version") ?? "",
162
+ revisionDate: root.attributes.get("revision-date") ?? "",
163
+ prefixes,
164
+ baseUnits,
165
+ units
166
+ };
167
+ }
168
+ function readUnit(element, code) {
169
+ const value = child(element, "value");
170
+ const unit = value?.attributes.get("Unit");
171
+ if (code === void 0 || value === void 0 || unit === void 0) return void 0;
172
+ const fn = child(value, "function");
173
+ let definition;
174
+ if (fn !== void 0) {
175
+ const name = fn.attributes.get("name");
176
+ const fnValue = fn.attributes.get("value");
177
+ const fnUnit = fn.attributes.get("Unit");
178
+ if (name === void 0 || fnValue === void 0 || fnUnit === void 0) return void 0;
179
+ definition = {
180
+ name,
181
+ value: fnValue,
182
+ unit: fnUnit
183
+ };
184
+ }
185
+ const isSpecial = element.attributes.get("isSpecial") === "yes";
186
+ if (isSpecial !== (definition !== void 0)) return void 0;
187
+ const factor = value.attributes.get("value");
188
+ if (!isSpecial && factor === void 0) return void 0;
189
+ return {
190
+ code,
191
+ isMetric: element.attributes.get("isMetric") === "yes",
192
+ isSpecial,
193
+ isArbitrary: element.attributes.get("isArbitrary") === "yes",
194
+ unit,
195
+ value: factor,
196
+ function: definition
197
+ };
198
+ }
199
+ function child(element, name) {
200
+ return element.children.find((candidate) => candidate.name === name);
201
+ }
202
+ // src/rational.ts
203
+ /**
204
+ * The largest numerator or denominator, in bits (about 21,000 decimal digits). A Decimal has at
205
+ * most 10,000 digits on either side of the point, so a ratio of 10^20000 or more already maps
206
+ * every non-zero Decimal outside that bound. Larger magnitudes are rejected before `BigInt` work
207
+ * grows with them.
208
+ */
209
+ const MAX_RATIONAL_BITS = 7e4;
210
+ /**
211
+ * The largest numerator or denominator of a unit magnitude, in bits (about 1,200 decimal digits).
212
+ * The largest magnitude in the essence file needs a few hundred bits, and a realistic unit term
213
+ * a few times more. Unit strings can come from untrusted data, and every `.` or `/` multiplies
214
+ * magnitudes, so this much smaller bound keeps each step cheap: without it, a 10 kB string of
215
+ * `[pi].[pi]…` would take about a minute of `BigInt` work before reaching the value bound.
216
+ */
217
+ const MAX_MAGNITUDE_BITS = 4096;
218
+ /** The rational 1. */
219
+ const ONE = Object.freeze({
220
+ numerator: 1n,
221
+ denominator: 1n
222
+ });
223
+ /**
224
+ * `numerator / denominator` in lowest terms, or `undefined` for a zero denominator or when either
225
+ * part needs more than `limit` bits.
226
+ */
227
+ function rational(numerator, denominator = 1n, limit = MAX_RATIONAL_BITS) {
228
+ if (denominator === 0n) return void 0;
229
+ if (denominator < 0n) {
230
+ numerator = -numerator;
231
+ denominator = -denominator;
232
+ }
233
+ const divisor = gcd(abs(numerator), denominator);
234
+ if (divisor > 1n) {
235
+ numerator /= divisor;
236
+ denominator /= divisor;
237
+ }
238
+ if (bitLength(numerator) > limit || bitLength(denominator) > limit) return;
239
+ return {
240
+ numerator,
241
+ denominator
242
+ };
243
+ }
244
+ /** `a × b`, or `undefined` when a part needs more than `limit` bits. */
245
+ function multiply(a, b, limit = MAX_RATIONAL_BITS) {
246
+ return rational(a.numerator * b.numerator, a.denominator * b.denominator, limit);
247
+ }
248
+ /** `a / b`, or `undefined` when `b` is zero or a part needs more than `limit` bits. */
249
+ function divide(a, b, limit = MAX_RATIONAL_BITS) {
250
+ return rational(a.numerator * b.denominator, a.denominator * b.numerator, limit);
251
+ }
252
+ /** `a + b`, or `undefined` on overflow. */
253
+ function add(a, b) {
254
+ return rational(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator);
255
+ }
256
+ /** `a - b`, or `undefined` on overflow. */
257
+ function subtract(a, b) {
258
+ return rational(a.numerator * b.denominator - b.numerator * a.denominator, a.denominator * b.denominator);
259
+ }
260
+ /**
261
+ * `base ^ exponent` for an integer exponent, or `undefined` for `0 ^ -n` and for results with a
262
+ * part beyond `limit` bits. The size is checked before the power is computed.
263
+ */
264
+ function power(base, exponent, limit = MAX_RATIONAL_BITS) {
265
+ if (!Number.isSafeInteger(exponent)) return void 0;
266
+ if (exponent === 0) return ONE;
267
+ const magnitude = Math.abs(exponent);
268
+ const size = Math.max(bitLength(base.numerator), bitLength(base.denominator));
269
+ if (size > 1 && (size - 1) * magnitude > limit) return void 0;
270
+ const n = BigInt(magnitude);
271
+ return exponent > 0 ? rational(base.numerator ** n, base.denominator ** n, limit) : rational(base.denominator ** n, base.numerator ** n, limit);
272
+ }
273
+ /** Orders two rationals exactly. */
274
+ function compare(a, b) {
275
+ const left = a.numerator * b.denominator;
276
+ const right = b.numerator * a.denominator;
277
+ return left < right ? -1 : left > right ? 1 : 0;
278
+ }
279
+ const DECIMAL_TEXT = /^([+-]?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/;
280
+ /**
281
+ * Parses decimal text with an optional exponent, such as `1e-3`, `980665e-5` or `3.14159`, as the
282
+ * exact rational it denotes. This is the number format of the essence file's `value` attributes.
283
+ */
284
+ function parseDecimalText(text) {
285
+ const match = DECIMAL_TEXT.exec(text);
286
+ if (match === null) return void 0;
287
+ const [, sign = "", integer = "", fraction = "", exponentText = "0"] = match;
288
+ const exponent = Number(exponentText) - fraction.length;
289
+ if (!Number.isSafeInteger(exponent) || Math.abs(exponent) * 3 > 7e4) return;
290
+ const digits = BigInt(integer + fraction) * (sign === "-" ? -1n : 1n);
291
+ const scale = 10n ** BigInt(Math.abs(exponent));
292
+ return exponent >= 0 ? rational(digits * scale) : rational(digits, scale);
293
+ }
294
+ /** The number of bits in `|value|`. */
295
+ function bitLength(value) {
296
+ return value === 0n ? 0 : abs(value).toString(2).length;
297
+ }
298
+ /** The greatest common divisor of two non-negative integers. */
299
+ function gcd(a, b) {
300
+ while (b !== 0n) [a, b] = [b, a % b];
301
+ return a;
302
+ }
303
+ function abs(value) {
304
+ return value < 0n ? -value : value;
305
+ }
306
+ // src/decimal-rational.ts
307
+ const COEFFICIENT_LIMIT = 10n ** BigInt(MAX_DECIMAL_DIGITS);
308
+ /** Whether `coefficient × 10^-scale` is within the Decimal size bound. */
309
+ function fits(coefficient, scale) {
310
+ return scale <= MAX_DECIMAL_DIGITS && coefficient < COEFFICIENT_LIMIT && -coefficient < COEFFICIENT_LIMIT;
311
+ }
312
+ /** The exact value of a Decimal as a rational. */
313
+ function decimalToRational(value) {
314
+ return rational(value.coefficient, 10n ** BigInt(value.scale));
315
+ }
316
+ /**
317
+ * Converts an exact result to a Decimal, as the minimal unit system does. A terminating result
318
+ * keeps every digit and at least `minScale` digits after the point, so `1.0 'm'` becomes
319
+ * `100.0 'cm'`; redundant zeros are only dropped when the value would not fit otherwise. A
320
+ * recurring result is rounded once, by Decimal division (28 significant digits).
321
+ *
322
+ * @returns the Decimal, or `undefined` if it is outside the Decimal size bound.
323
+ * @see https://hl7.org/fhirpath/STU3/en/index.html#unit-conversions
324
+ */
325
+ function rationalToDecimal(value, minScale) {
326
+ let rest = value.denominator;
327
+ let twos = 0;
328
+ let fives = 0;
329
+ while (rest % 2n === 0n) {
330
+ rest /= 2n;
331
+ twos++;
332
+ }
333
+ while (rest % 5n === 0n) {
334
+ rest /= 5n;
335
+ fives++;
336
+ }
337
+ const places = Math.max(twos, fives);
338
+ if (places > MAX_DECIMAL_DIGITS) return void 0;
339
+ let coefficient = value.numerator * 2n ** BigInt(places - twos) * 5n ** BigInt(places - fives);
340
+ if (rest !== 1n) return fits(coefficient, places) && fits(rest, 0) ? Decimal.of(coefficient, places).divide(Decimal.of(rest)) : void 0;
341
+ let scale = places;
342
+ if (scale < minScale) {
343
+ coefficient *= 10n ** BigInt(minScale - scale);
344
+ scale = minScale;
345
+ }
346
+ while (!fits(coefficient, scale) && scale > 0 && coefficient % 10n === 0n) {
347
+ coefficient /= 10n;
348
+ scale--;
349
+ }
350
+ return fits(coefficient, scale) ? Decimal.of(coefficient, scale) : void 0;
351
+ }
352
+ /**
353
+ * A strictly positive scale as `factor` and optional `divisor` Decimals: a terminating scale is a
354
+ * single exact factor, and a recurring one such as 1/60 keeps both integers so that callers can
355
+ * divide once at the end.
356
+ */
357
+ function rationalToScale(value) {
358
+ if (value.numerator <= 0n) return void 0;
359
+ const exact = isTerminating(value.denominator) ? rationalToDecimal(value, 0) : void 0;
360
+ if (exact !== void 0) return { factor: exact };
361
+ return fits(value.numerator, 0) && fits(value.denominator, 0) ? {
362
+ factor: Decimal.of(value.numerator),
363
+ divisor: Decimal.of(value.denominator)
364
+ } : void 0;
365
+ }
366
+ function isTerminating(denominator) {
367
+ let rest = denominator;
368
+ while (rest % 2n === 0n) rest /= 2n;
369
+ while (rest % 5n === 0n) rest /= 5n;
370
+ return rest === 1n;
371
+ }
372
+ // src/dimension.ts
373
+ /** The dimension of the unity `1`. */
374
+ const DIMENSIONLESS = /* @__PURE__ */ new Map();
375
+ /** The dimension with one axis at exponent 1. */
376
+ function axis(code) {
377
+ return /* @__PURE__ */ new Map([[code, 1]]);
378
+ }
379
+ /**
380
+ * `a × b^exponent`, or `undefined` if an exponent leaves the safe integer range.
381
+ */
382
+ function combineDimensions(a, b, exponent = 1) {
383
+ const result = new Map(a);
384
+ for (const [code, value] of b) {
385
+ const product = value * exponent;
386
+ const sum = (result.get(code) ?? 0) + product;
387
+ if (!Number.isSafeInteger(product) || !Number.isSafeInteger(sum)) return void 0;
388
+ if (sum === 0) result.delete(code);
389
+ else result.set(code, sum);
390
+ }
391
+ return result;
392
+ }
393
+ /** Whether two dimensions are the same, so that their units are commensurable. */
394
+ function sameDimension(a, b) {
395
+ if (a.size !== b.size) return false;
396
+ for (const [code, value] of a) if (b.get(code) !== value) return false;
397
+ return true;
398
+ }
399
+ /** Characters that end a symbol outside square brackets. */
400
+ const DELIMITERS = /* @__PURE__ */ new Set([
401
+ ".",
402
+ "/",
403
+ "(",
404
+ ")",
405
+ "{",
406
+ "}"
407
+ ]);
408
+ const DIGITS = /^[0-9]+$/;
409
+ /**
410
+ * Parses the syntax of a case-sensitive UCUM unit expression without looking up any symbol, so
411
+ * `xyz` parses here and is rejected later. Whitespace and characters outside ASCII 33–126 are
412
+ * invalid.
413
+ *
414
+ * @returns the term, or `undefined` if the text is not a syntactically valid unit expression.
415
+ * @see https://ucum.org/ucum#section-Syntax-Rules
416
+ */
417
+ function parseUnitExpression(text) {
418
+ if (text.length === 0) return void 0;
419
+ for (let index = 0; index < text.length; index++) {
420
+ const code = text.charCodeAt(index);
421
+ if (code < 33 || code > 126) return void 0;
422
+ }
423
+ const leading = text.startsWith("/");
424
+ const cursor = { index: leading ? 1 : 0 };
425
+ const term = parseTerm(text, cursor, leading ? -1 : 1, 0);
426
+ return term !== void 0 && cursor.index === text.length ? term : void 0;
427
+ }
428
+ function parseTerm(text, cursor, first, depth) {
429
+ const factors = [];
430
+ let operator = first;
431
+ for (;;) {
432
+ const component = parseComponent(text, cursor, depth);
433
+ if (component === void 0) return void 0;
434
+ factors.push({
435
+ operator,
436
+ component
437
+ });
438
+ const next = text[cursor.index];
439
+ if (next === ".") operator = 1;
440
+ else if (next === "/") operator = -1;
441
+ else return { factors };
442
+ cursor.index++;
443
+ }
444
+ }
445
+ function parseComponent(text, cursor, depth) {
446
+ const start = cursor.index;
447
+ const first = text[start];
448
+ if (first === "(") {
449
+ if (depth >= 32) return void 0;
450
+ cursor.index++;
451
+ const term = parseTerm(text, cursor, 1, depth + 1);
452
+ if (term === void 0 || text[cursor.index] !== ")") return void 0;
453
+ cursor.index++;
454
+ return {
455
+ kind: "group",
456
+ term
457
+ };
458
+ }
459
+ if (first === "{") {
460
+ const annotation = parseAnnotation(text, cursor);
461
+ return annotation === void 0 ? void 0 : {
462
+ kind: "annotation",
463
+ text: annotation
464
+ };
465
+ }
466
+ let index = start;
467
+ let brackets = 0;
468
+ for (; index < text.length; index++) {
469
+ const char = text.charAt(index);
470
+ if (char === "[") brackets++;
471
+ else if (char === "]") {
472
+ if (brackets === 0) return void 0;
473
+ brackets--;
474
+ } else if (brackets === 0 && DELIMITERS.has(char)) break;
475
+ }
476
+ if (brackets !== 0 || index === start) return void 0;
477
+ const run = text.slice(start, index);
478
+ cursor.index = index;
479
+ if (DIGITS.test(run)) return {
480
+ kind: "number",
481
+ value: BigInt(run),
482
+ exponent: 1
483
+ };
484
+ const split = exponentStart(run);
485
+ const symbol = run.slice(0, split);
486
+ const exponent = split === run.length ? 1 : Number(run.slice(split));
487
+ if (!Number.isSafeInteger(exponent)) return void 0;
488
+ if (DIGITS.test(symbol)) return {
489
+ kind: "number",
490
+ value: BigInt(symbol),
491
+ exponent
492
+ };
493
+ let annotation;
494
+ if (text[cursor.index] === "{") {
495
+ annotation = parseAnnotation(text, cursor);
496
+ if (annotation === void 0) return void 0;
497
+ }
498
+ return {
499
+ kind: "symbol",
500
+ symbol,
501
+ exponent,
502
+ annotation
503
+ };
504
+ }
505
+ /**
506
+ * Where the exponent of a symbol starts: its trailing digits with an optional sign (§9), or the
507
+ * end of the symbol if it has none. A linear scan, so long input cannot cause backtracking.
508
+ */
509
+ function exponentStart(run) {
510
+ let start = run.length;
511
+ while (start > 0 && isDigit(run.charCodeAt(start - 1))) start--;
512
+ if (start === run.length) return run.length;
513
+ const sign = run[start - 1];
514
+ if (sign === "+" || sign === "-") start--;
515
+ return start === 0 ? run.length : start;
516
+ }
517
+ function isDigit(code) {
518
+ return code >= 48 && code <= 57;
519
+ }
520
+ /** Reads `{…}` at the cursor; the text inside may not contain another curly brace. */
521
+ function parseAnnotation(text, cursor) {
522
+ const end = text.indexOf("}", cursor.index + 1);
523
+ if (end === -1) return void 0;
524
+ const annotation = text.slice(cursor.index + 1, end);
525
+ if (annotation.includes("{")) return void 0;
526
+ cursor.index = end + 1;
527
+ return annotation;
528
+ }
529
+ // src/resolve-unit.ts
530
+ const UNSUPPORTED = Object.freeze({ kind: "unsupported" });
531
+ /**
532
+ * Resolves a case-sensitive UCUM expression such as `mg/dL` or `kg.m/s2` against the symbol
533
+ * tables. Annotations are ignored (§12).
534
+ *
535
+ * @returns the unit, or `undefined` if it is invalid, uses an unknown symbol, has a zero
536
+ * number, or a magnitude on the way needs more than {@link MAX_MAGNITUDE_BITS} bits.
537
+ * @see https://ucum.org/ucum#section-Semantics
538
+ */
539
+ function resolveUnit(text, lookup) {
540
+ const syntax = parseUnitExpression(text);
541
+ if (syntax === void 0) return void 0;
542
+ const state = {
543
+ magnitude: ONE,
544
+ dimension: DIMENSIONLESS,
545
+ scalar: ONE,
546
+ arbitrary: false,
547
+ special: false,
548
+ terms: []
549
+ };
550
+ if (!accumulate(syntax, 1, state, lookup)) return void 0;
551
+ if (state.special) return resolveSpecial(syntax, lookup);
552
+ return {
553
+ magnitude: state.magnitude,
554
+ dimension: state.dimension,
555
+ terms: state.terms,
556
+ scalar: state.scalar,
557
+ arbitrary: state.arbitrary,
558
+ special: void 0
559
+ };
560
+ }
561
+ function accumulate(term, sign, state, lookup) {
562
+ for (const { operator, component } of term.factors) {
563
+ const direction = sign === operator ? 1 : -1;
564
+ switch (component.kind) {
565
+ case "annotation": break;
566
+ case "group":
567
+ if (!accumulate(component.term, direction, state, lookup)) return false;
568
+ break;
569
+ case "number": {
570
+ const value = power({
571
+ numerator: component.value,
572
+ denominator: 1n
573
+ }, component.exponent, MAX_MAGNITUDE_BITS);
574
+ const scaled = value === void 0 ? void 0 : power(value, direction);
575
+ if (scaled === void 0 || scaled.numerator === 0n) return false;
576
+ const magnitude = multiply(state.magnitude, scaled, MAX_MAGNITUDE_BITS);
577
+ const scalar = multiply(state.scalar, scaled, MAX_MAGNITUDE_BITS);
578
+ if (magnitude === void 0 || scalar === void 0) return false;
579
+ state.magnitude = magnitude;
580
+ state.scalar = scalar;
581
+ break;
582
+ }
583
+ case "symbol": {
584
+ const unit = simpleUnit(component.symbol, lookup);
585
+ if (unit === void 0) return false;
586
+ if (unit.atom.special !== void 0) {
587
+ state.special = true;
588
+ break;
589
+ }
590
+ const exponent = component.exponent * direction;
591
+ const perUnit = multiply(unit.prefix, unit.atom.magnitude, MAX_MAGNITUDE_BITS);
592
+ const scaled = perUnit === void 0 ? void 0 : power(perUnit, exponent, MAX_MAGNITUDE_BITS);
593
+ const magnitude = scaled === void 0 ? void 0 : multiply(state.magnitude, scaled, MAX_MAGNITUDE_BITS);
594
+ const dimension = combineDimensions(state.dimension, unit.atom.dimension, exponent);
595
+ if (perUnit === void 0 || magnitude === void 0 || dimension === void 0) return false;
596
+ state.magnitude = magnitude;
597
+ state.dimension = dimension;
598
+ state.arbitrary ||= unit.atom.arbitrary;
599
+ state.terms.push({
600
+ symbol: component.symbol,
601
+ exponent,
602
+ magnitude: perUnit,
603
+ dimension: unit.atom.dimension,
604
+ arbitrary: unit.atom.arbitrary
605
+ });
606
+ break;
607
+ }
608
+ default: return assertNever(component);
609
+ }
610
+ }
611
+ return true;
612
+ }
613
+ /**
614
+ * Special units take no part in algebra (§22■1). Only a lone, unprefixed special unit with
615
+ * exponent 1, such as `Cel` or `Cel{body}`, keeps its scale.
616
+ */
617
+ function resolveSpecial(syntax, lookup) {
618
+ const [only, ...rest] = syntax.factors;
619
+ const component = only?.component;
620
+ const atom = rest.length === 0 && only?.operator === 1 && component?.kind === "symbol" && component.exponent === 1 ? lookup.atom(component.symbol) : void 0;
621
+ if (atom?.special?.kind === "affine") return {
622
+ magnitude: atom.magnitude,
623
+ dimension: atom.dimension,
624
+ terms: [],
625
+ scalar: ONE,
626
+ arbitrary: false,
627
+ special: atom.special
628
+ };
629
+ return {
630
+ magnitude: ONE,
631
+ dimension: DIMENSIONLESS,
632
+ terms: [],
633
+ scalar: ONE,
634
+ arbitrary: false,
635
+ special: UNSUPPORTED
636
+ };
637
+ }
638
+ /**
639
+ * Splits a simple unit into prefix and atom. The prefix is the longest one whose remainder is a
640
+ * metric atom; without such a prefix the whole symbol must be an atom (§4■4).
641
+ */
642
+ function simpleUnit(symbol, lookup) {
643
+ for (const code of lookup.prefixCodes) {
644
+ if (symbol.length <= code.length || !symbol.startsWith(code)) continue;
645
+ const rest = symbol.slice(code.length);
646
+ if (!lookup.hasAtom(rest) || !lookup.isMetric(rest)) continue;
647
+ const atom = lookup.atom(rest);
648
+ const prefix = lookup.prefix(code);
649
+ return atom === void 0 || prefix === void 0 ? void 0 : {
650
+ prefix,
651
+ atom
652
+ };
653
+ }
654
+ const atom = lookup.atom(symbol);
655
+ return atom === void 0 ? void 0 : {
656
+ prefix: ONE,
657
+ atom
658
+ };
659
+ }
660
+ function assertNever(value) {
661
+ return value;
662
+ }
663
+ // src/ucum-unit-system.ts
664
+ /**
665
+ * The UCUM counterpart of each calendar keyword. Week to millisecond are equal to theirs; year and
666
+ * month are only equivalent to `a` and `mo`.
667
+ * @see https://hl7.org/fhirpath/STU3/#time-valued-quantities
668
+ */
669
+ const CALENDAR_UCUM = Object.freeze({
670
+ year: "a",
671
+ month: "mo",
672
+ week: "wk",
673
+ day: "d",
674
+ hour: "h",
675
+ minute: "min",
676
+ second: "s",
677
+ millisecond: "ms"
678
+ });
679
+ /**
680
+ * Creates a UCUM unit system from resolved symbol tables.
681
+ *
682
+ * Calendar keywords and the units the minimal unit system knows (`1` and the UCUM time units)
683
+ * are handed to `minimalUnitSystem` whenever both units of an operation are among them, so the
684
+ * calendar rules stay exactly as in core. Other units are resolved as UCUM expressions:
685
+ * commensurable units convert exactly, `Cel` and `[degF]` convert on their offset scales, and
686
+ * arbitrary or other special units only match the identical unit.
687
+ */
688
+ function createUcumUnitSystem(lookup) {
689
+ const resolve = (unit) => resolveUnit(unit, lookup);
690
+ function convertUcum(value, from, to) {
691
+ const source = commensurable(resolve(from), resolve(to));
692
+ if (source === void 0) return void 0;
693
+ const exact = decimalToRational(value);
694
+ const base = exact === void 0 ? void 0 : toBase(exact, source[0]);
695
+ const result = base === void 0 ? void 0 : fromBase(base, source[1]);
696
+ return result === void 0 ? void 0 : rationalToDecimal(result, value.scale);
697
+ }
698
+ function compareUcum(a, from, b, to) {
699
+ const units = commensurable(resolve(from), resolve(to));
700
+ if (units === void 0) return void 0;
701
+ const left = decimalToRational(a);
702
+ const right = decimalToRational(b);
703
+ const leftBase = left === void 0 ? void 0 : toBase(left, units[0]);
704
+ const rightBase = right === void 0 ? void 0 : toBase(right, units[1]);
705
+ return leftBase === void 0 || rightBase === void 0 ? void 0 : compare(leftBase, rightBase);
706
+ }
707
+ function algebra(a, b, sign) {
708
+ const left = resolve(a);
709
+ const right = resolve(b);
710
+ if (left?.special !== void 0 || right?.special !== void 0) return void 0;
711
+ if (b === "1") return {
712
+ unit: a,
713
+ factor: Decimal.ONE
714
+ };
715
+ if (sign === 1 && a === "1") return {
716
+ unit: b,
717
+ factor: Decimal.ONE
718
+ };
719
+ if (sign === -1 && a === b) return {
720
+ unit: "1",
721
+ factor: Decimal.ONE
722
+ };
723
+ if (left === void 0 || right === void 0) return void 0;
724
+ const product = canonicalProduct(left, right, sign);
725
+ return product === void 0 || resolve(product.unit) === void 0 ? void 0 : product;
726
+ }
727
+ return Object.freeze({
728
+ parse(unit) {
729
+ return minimalUnitSystem.parse(unit) ?? (resolve(unit) === void 0 ? void 0 : { unit });
730
+ },
731
+ convert(value, from, to, options) {
732
+ const source = calendarKeyword(from);
733
+ const target = calendarKeyword(to);
734
+ if ((source ?? from) === (target ?? to)) return value;
735
+ if (isMinimalUnit(from) && isMinimalUnit(to)) return minimalUnitSystem.convert(value, from, to, options);
736
+ const explicit = options?.explicit === true;
737
+ const equivalence = !explicit && options?.equivalence === true;
738
+ const a = source === void 0 ? from : ucumCounterpart(source, equivalence);
739
+ const b = target === void 0 ? to : ucumCounterpart(target, equivalence || explicit);
740
+ return a === void 0 || b === void 0 ? void 0 : convertUcum(value, a, b);
741
+ },
742
+ compare(a, from, b, to) {
743
+ const source = calendarKeyword(from);
744
+ const target = calendarKeyword(to);
745
+ if ((source ?? from) === (target ?? to)) return a.compare(b);
746
+ if (isMinimalUnit(from) && isMinimalUnit(to)) return minimalUnitSystem.compare?.(a, from, b, to);
747
+ const x = source === void 0 ? from : ucumCounterpart(source, false);
748
+ const y = target === void 0 ? to : ucumCounterpart(target, false);
749
+ return x === void 0 || y === void 0 ? void 0 : compareUcum(a, x, b, y);
750
+ },
751
+ multiply(a, b) {
752
+ if (calendarKeyword(a) !== void 0 || calendarKeyword(b) !== void 0) return minimalUnitSystem.multiply(a, b);
753
+ return algebra(a, b, 1);
754
+ },
755
+ divide(a, b) {
756
+ if (calendarKeyword(a) !== void 0 || calendarKeyword(b) !== void 0) return minimalUnitSystem.divide(a, b);
757
+ return algebra(a, b, -1);
758
+ }
759
+ });
760
+ }
761
+ /** Both units if they can be converted into each other, else `undefined`. */
762
+ function commensurable(a, b) {
763
+ if (a === void 0 || b === void 0) return void 0;
764
+ if (a.special?.kind === "unsupported" || b.special?.kind === "unsupported") return void 0;
765
+ if (a.arbitrary || b.arbitrary) return void 0;
766
+ return sameDimension(a.dimension, b.dimension) ? [a, b] : void 0;
767
+ }
768
+ /** The value in base units: `(value + offset) × magnitude` for `Cel` and `[degF]`. */
769
+ function toBase(value, unit) {
770
+ const shifted = unit.special?.kind === "affine" ? add(value, unit.special.offset) : value;
771
+ return shifted === void 0 ? void 0 : multiply(shifted, unit.magnitude);
772
+ }
773
+ function fromBase(base, unit) {
774
+ const scaled = divide(base, unit.magnitude);
775
+ if (scaled === void 0 || unit.special?.kind !== "affine") return scaled;
776
+ return subtract(scaled, unit.special.offset);
777
+ }
778
+ /**
779
+ * The product or quotient of two units in canonical form. Identical simple units add their
780
+ * exponents (`cm2 / cm` is `cm`); a simple unit commensurable with an earlier one that has not
781
+ * cancelled out is converted into it (`m/s × min` is `m` with factor 60, and `cm / m` is `1` with
782
+ * factor 1/100). Arbitrary and dimensionless units only combine with identical ones. Integer
783
+ * factors and annotations are folded into the scale.
784
+ * @see https://hl7.org/fhirpath/STU3/en/index.html#multiplication
785
+ */
786
+ function canonicalProduct(left, right, sign) {
787
+ let scale = sign === 1 ? multiply(left.scalar, right.scalar, MAX_MAGNITUDE_BITS) : divide(left.scalar, right.scalar, MAX_MAGNITUDE_BITS);
788
+ const terms = [];
789
+ const bySymbol = /* @__PURE__ */ new Map();
790
+ const byDimension = /* @__PURE__ */ new Map();
791
+ const inputs = [...left.terms.map((term) => ({
792
+ term,
793
+ exponent: term.exponent
794
+ })), ...right.terms.map((term) => ({
795
+ term,
796
+ exponent: term.exponent * sign
797
+ }))];
798
+ for (const input of inputs) {
799
+ if (scale === void 0) return void 0;
800
+ const merges = !input.term.arbitrary && input.term.dimension.size > 0;
801
+ const key = merges ? dimensionKey(input.term.dimension) : "";
802
+ const same = bySymbol.get(input.term.symbol);
803
+ const target = same ?? (merges ? byDimension.get(key) : void 0);
804
+ if (target === void 0) {
805
+ const entry = {
806
+ term: input.term,
807
+ exponent: input.exponent
808
+ };
809
+ terms.push(entry);
810
+ bySymbol.set(input.term.symbol, entry);
811
+ if (merges) byDimension.set(key, entry);
812
+ continue;
813
+ }
814
+ if (target !== same) {
815
+ const ratio = divide(input.term.magnitude, target.term.magnitude, MAX_MAGNITUDE_BITS);
816
+ const factor = ratio === void 0 ? void 0 : power(ratio, input.exponent, MAX_MAGNITUDE_BITS);
817
+ scale = factor === void 0 ? void 0 : multiply(scale, factor, MAX_MAGNITUDE_BITS);
818
+ }
819
+ target.exponent += input.exponent;
820
+ if (!Number.isSafeInteger(target.exponent)) return void 0;
821
+ if (target.exponent === 0) {
822
+ bySymbol.delete(target.term.symbol);
823
+ if (byDimension.get(key) === target) byDimension.delete(key);
824
+ }
825
+ }
826
+ if (scale === void 0) return void 0;
827
+ const parts = rationalToScale(scale);
828
+ if (parts === void 0) return void 0;
829
+ const unit = formatTerms(terms);
830
+ return parts.divisor === void 0 ? {
831
+ unit,
832
+ factor: parts.factor
833
+ } : {
834
+ unit,
835
+ factor: parts.factor,
836
+ divisor: parts.divisor
837
+ };
838
+ }
839
+ /** A string that two dimensions share exactly when they are the same. */
840
+ function dimensionKey(dimension) {
841
+ return JSON.stringify([...dimension].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
842
+ }
843
+ /** Writes terms as `a.b2/c/d3`, or `/s` without a numerator, or `1` when nothing is left. */
844
+ function formatTerms(terms) {
845
+ let numerator = "";
846
+ let denominator = "";
847
+ for (const { term, exponent } of terms) if (exponent > 0) numerator += `${numerator === "" ? "" : "."}${term.symbol}${exponent === 1 ? "" : exponent}`;
848
+ else if (exponent < 0) denominator += `/${term.symbol}${exponent === -1 ? "" : -exponent}`;
849
+ return numerator === "" && denominator === "" ? "1" : numerator + denominator;
850
+ }
851
+ /** The singular calendar keyword a unit spells, if any (`weeks` is `week`). */
852
+ function calendarKeyword(unit) {
853
+ const singular = unit.endsWith("s") ? unit.slice(0, -1) : unit;
854
+ if (Object.hasOwn(CALENDAR_UCUM, unit)) return unit;
855
+ return Object.hasOwn(CALENDAR_UCUM, singular) ? singular : void 0;
856
+ }
857
+ /** The UCUM unit a calendar keyword may be related to; year/month only with equivalence. */
858
+ function ucumCounterpart(keyword, yearAndMonth) {
859
+ if ((keyword === "year" || keyword === "month") && !yearAndMonth) return void 0;
860
+ return Object.hasOwn(CALENDAR_UCUM, keyword) ? CALENDAR_UCUM[keyword] : void 0;
861
+ }
862
+ /** Units the minimal unit system knows: `1`, calendar keywords and the UCUM time units. */
863
+ function isMinimalUnit(unit) {
864
+ return minimalUnitSystem.parse(unit) !== void 0;
865
+ }
866
+ // src/unit-table.ts
867
+ /**
868
+ * The special-unit functions Callis converts, by function name in the essence file. Each is the
869
+ * affine relation `proper = (value + offset) × magnitude`: `Cel` gives kelvin = °C + 273.15, and
870
+ * `degF` gives (5/9 K) multiples = °F + 459.67. The essence file names these functions; their
871
+ * definitions come from the UCUM specification text. Other special units are not convertible.
872
+ * @see https://ucum.org/ucum#section-Special-Units-on-non-ratio-Scales
873
+ */
874
+ const AFFINE_OFFSETS = Object.freeze({
875
+ Cel: Object.freeze({
876
+ numerator: 5463n,
877
+ denominator: 20n
878
+ }),
879
+ degF: Object.freeze({
880
+ numerator: 45967n,
881
+ denominator: 100n
882
+ })
883
+ });
884
+ /** A lookup that knows no units, for when the essence data cannot be read. */
885
+ const EMPTY_UNIT_LOOKUP = Object.freeze({
886
+ hasAtom: () => false,
887
+ isMetric: () => false,
888
+ atom: () => void 0,
889
+ prefix: () => void 0,
890
+ prefixCodes: Object.freeze([])
891
+ });
892
+ /**
893
+ * Resolves every prefix, base unit and unit of the essence data to a magnitude and dimension in
894
+ * base units. Definitions refer to other units, so each atom is resolved once, on first need.
895
+ * An atom whose definition is invalid, circular or out of bounds is left out.
896
+ */
897
+ function buildUnitTable(data) {
898
+ const prefixes = /* @__PURE__ */ new Map();
899
+ for (const prefix of data.prefixes) {
900
+ const value = parseDecimalText(prefix.value);
901
+ if (value !== void 0 && value.numerator > 0n) prefixes.set(prefix.code, value);
902
+ }
903
+ const atoms = /* @__PURE__ */ new Map();
904
+ for (const base of data.baseUnits) atoms.set(base.code, {
905
+ code: base.code,
906
+ isMetric: true,
907
+ magnitude: ONE,
908
+ dimension: axis(base.code),
909
+ arbitrary: false,
910
+ special: void 0
911
+ });
912
+ const definitions = /* @__PURE__ */ new Map();
913
+ for (const unit of data.units) if (!atoms.has(unit.code) && !definitions.has(unit.code)) definitions.set(unit.code, unit);
914
+ const prefixCodes = Object.freeze([...prefixes.keys()].sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0)));
915
+ const pending = /* @__PURE__ */ new Set();
916
+ const failed = /* @__PURE__ */ new Set();
917
+ const lookup = {
918
+ hasAtom: (code) => atoms.has(code) || definitions.has(code),
919
+ isMetric: (code) => atoms.get(code)?.isMetric ?? definitions.get(code)?.isMetric ?? false,
920
+ atom: (code) => {
921
+ const known = atoms.get(code);
922
+ if (known !== void 0) return known;
923
+ const definition = definitions.get(code);
924
+ if (definition === void 0 || pending.has(code) || failed.has(code)) return void 0;
925
+ pending.add(code);
926
+ const atom = defineAtom(definition, lookup);
927
+ pending.delete(code);
928
+ if (atom === void 0) failed.add(code);
929
+ else atoms.set(code, atom);
930
+ return atom;
931
+ },
932
+ prefix: (code) => prefixes.get(code),
933
+ prefixCodes
934
+ };
935
+ for (const code of definitions.keys()) lookup.atom(code);
936
+ return Object.freeze({
937
+ hasAtom: (code) => atoms.has(code),
938
+ isMetric: (code) => atoms.get(code)?.isMetric ?? false,
939
+ atom: (code) => atoms.get(code),
940
+ prefix: (code) => prefixes.get(code),
941
+ prefixCodes
942
+ });
943
+ }
944
+ function defineAtom(definition, lookup) {
945
+ const source = definition.function ?? {
946
+ value: definition.value ?? "",
947
+ unit: definition.unit
948
+ };
949
+ const value = parseDecimalText(source.value);
950
+ const unit = resolveUnit(source.unit, lookup);
951
+ if (value === void 0 || value.numerator <= 0n || unit === void 0 || unit.special !== void 0) return;
952
+ const magnitude = multiply(value, unit.magnitude, MAX_MAGNITUDE_BITS);
953
+ if (magnitude === void 0) return void 0;
954
+ let special;
955
+ if (definition.function !== void 0) {
956
+ const name = definition.function.name;
957
+ const offset = Object.hasOwn(AFFINE_OFFSETS, name) ? AFFINE_OFFSETS[name] : void 0;
958
+ special = offset === void 0 ? { kind: "unsupported" } : {
959
+ kind: "affine",
960
+ offset
961
+ };
962
+ }
963
+ const dimension = definition.isArbitrary ? combineDimensions(unit.dimension, axis(definition.code)) : unit.dimension;
964
+ if (dimension === void 0) return void 0;
965
+ return {
966
+ code: definition.code,
967
+ isMetric: definition.isMetric,
968
+ magnitude,
969
+ dimension,
970
+ arbitrary: definition.isArbitrary || unit.arbitrary,
971
+ special
972
+ };
973
+ }
974
+ // src/index.ts
975
+ /**
976
+ * A `UnitSystem` with the full UCUM: prefixes, every unit of the essence file, unit expressions
977
+ * such as `mg/dL` or `kg.m/s2`, annotations, `10*n` and `10^n`. Commensurable units convert
978
+ * exactly; `Cel` and `[degF]` convert on their offset scales; arbitrary units and other special
979
+ * units only match the identical unit. Calendar keywords keep the rules of the minimal unit
980
+ * system in `@callis/core`. Unknown or invalid units give `undefined`; nothing throws.
981
+ *
982
+ * @example
983
+ * ```ts
984
+ * import { Decimal } from '@callis/core';
985
+ * import { ucum } from '@callis/ucum';
986
+ *
987
+ * ucum.convert(Decimal.ONE, 'mg/dL', 'g/L')?.toString(); // '0.01'
988
+ * ucum.compare?.(Decimal.ONE, 'kg', Decimal.of(1000n), 'g'); // 0
989
+ * ```
990
+ * @see https://ucum.org/ucum
991
+ * @see https://hl7.org/fhirpath/STU3/#quantity
992
+ */
993
+ const ucum = /* @__PURE__ */ createUcumUnitSystem(/* @__PURE__ */ loadUnitTable());
994
+ function loadUnitTable() {
995
+ const essence = readEssence(UCUM_ESSENCE_XML);
996
+ return essence === void 0 ? EMPTY_UNIT_LOOKUP : buildUnitTable(essence);
997
+ }
998
+ export { ucum };
999
+
1000
+ //# sourceMappingURL=index.js.map