@callis/ucum 0.0.0-stage → 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/CHANGELOG.md +21 -0
- package/LICENSE.md +39 -0
- package/README.md +55 -2
- package/SOURCE.md +32 -0
- package/UCUM-LICENSE.md +283 -0
- package/data/source.json +20 -0
- package/data/ucum-essence.xml +2059 -0
- package/dist/essence/ucum-essence-xml.d.ts +3 -0
- package/dist/essence/ucum-essence-xml.js +21 -0
- package/dist/essence/ucum-essence-xml.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1000 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -3
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
|