@weasel-js/quantity 1.6.1
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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +430 -0
- package/dist/index.js +765 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// src/quantity.ts
|
|
2
|
+
function amount(q) {
|
|
3
|
+
return typeof q === "number" ? q : q.value;
|
|
4
|
+
}
|
|
5
|
+
function retag(q, next) {
|
|
6
|
+
return typeof q === "number" ? next : { ...q, value: next };
|
|
7
|
+
}
|
|
8
|
+
function tag(value, display, unit2) {
|
|
9
|
+
const t = { value };
|
|
10
|
+
if (unit2 !== void 0) t.unit = unit2;
|
|
11
|
+
if (display !== void 0) t.display = display;
|
|
12
|
+
return t;
|
|
13
|
+
}
|
|
14
|
+
function displayOf(q, fallback) {
|
|
15
|
+
return (typeof q === "number" ? void 0 : q.display) ?? fallback;
|
|
16
|
+
}
|
|
17
|
+
function unitOf(q) {
|
|
18
|
+
return typeof q === "number" ? void 0 : q.unit;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/units.ts
|
|
22
|
+
function unitScale(unitSystem, unit2) {
|
|
23
|
+
const entry = unitSystem.units[unit2];
|
|
24
|
+
if (entry === void 0) {
|
|
25
|
+
const known = Object.keys(unitSystem.units).join(", ") || "(none)";
|
|
26
|
+
throw new Error(
|
|
27
|
+
`unknown unit '${unit2}' (system base: '${unitSystem.base}', known units: ${known})`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return entryScale(entry);
|
|
31
|
+
}
|
|
32
|
+
function entryScale(entry) {
|
|
33
|
+
return typeof entry === "number" ? { factor: entry, offset: 0 } : { factor: entry.factor, offset: entry.offset ?? 0 };
|
|
34
|
+
}
|
|
35
|
+
function resolveUnit(v, unitSystem) {
|
|
36
|
+
if (typeof v === "number") return v;
|
|
37
|
+
if (v.unit === void 0) return v.value;
|
|
38
|
+
if (!unitSystem) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`resolveUnit: tagged value { value: ${v.value}, unit: '${v.unit}' } requires a UnitSystem`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const { factor, offset } = scaleOrThrow("resolveUnit", unitSystem, v.unit);
|
|
44
|
+
return v.value * factor + offset;
|
|
45
|
+
}
|
|
46
|
+
function formatUnit(baseValue, displayUnit, unitSystem, opts) {
|
|
47
|
+
const { factor, offset } = scaleOrThrow("formatUnit", unitSystem, displayUnit);
|
|
48
|
+
const precision = opts?.precision ?? 2;
|
|
49
|
+
const suffix = opts?.suffix ?? true;
|
|
50
|
+
const display = (baseValue - offset) / factor;
|
|
51
|
+
let s = display.toFixed(precision);
|
|
52
|
+
if (s.includes(".")) {
|
|
53
|
+
s = s.replace(/0+$/, "").replace(/\.$/, "");
|
|
54
|
+
}
|
|
55
|
+
return suffix ? `${s}${displayUnit}` : s;
|
|
56
|
+
}
|
|
57
|
+
function scaleOrThrow(caller, unitSystem, unit2) {
|
|
58
|
+
try {
|
|
59
|
+
return unitScale(unitSystem, unit2);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw new Error(`${caller}: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var IMPERIAL_INCHES = {
|
|
65
|
+
base: "in",
|
|
66
|
+
units: { in: 1, ft: 12, yd: 36, mi: 63360 }
|
|
67
|
+
};
|
|
68
|
+
var METRIC_MM = {
|
|
69
|
+
base: "mm",
|
|
70
|
+
units: { mm: 1, cm: 10, m: 1e3, km: 1e6 }
|
|
71
|
+
};
|
|
72
|
+
var ANGLE_RADIANS = {
|
|
73
|
+
base: "rad",
|
|
74
|
+
units: { rad: 1, deg: Math.PI / 180, turn: Math.PI * 2 }
|
|
75
|
+
};
|
|
76
|
+
var PIXELS = {
|
|
77
|
+
base: "px",
|
|
78
|
+
units: { px: 1 }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/number.ts
|
|
82
|
+
var MINUS_SIGN = "\u2212";
|
|
83
|
+
function formatNumber(value, options) {
|
|
84
|
+
const formatted = Number.isFinite(value) ? value.toLocaleString(void 0, options) : String(value);
|
|
85
|
+
return formatted.replace(/^-/, MINUS_SIGN);
|
|
86
|
+
}
|
|
87
|
+
function parseSignedNumber(text) {
|
|
88
|
+
return Number(text.replace(MINUS_SIGN, "-"));
|
|
89
|
+
}
|
|
90
|
+
var EXPONENTS = { k: 3, m: 6, b: 9, t: 12 };
|
|
91
|
+
function formatCompact(value, decimals = 0) {
|
|
92
|
+
if (!Number.isFinite(value)) return formatNumber(value);
|
|
93
|
+
const options = Math.abs(value) < 1e3 ? { useGrouping: false, minimumFractionDigits: decimals, maximumFractionDigits: decimals } : { notation: "compact", minimumSignificantDigits: 3, maximumSignificantDigits: 3 };
|
|
94
|
+
return value.toLocaleString("en-US", options).replace(/^-/, MINUS_SIGN);
|
|
95
|
+
}
|
|
96
|
+
function parseNumber(text, units) {
|
|
97
|
+
if (units) {
|
|
98
|
+
const trimmed = text.trim();
|
|
99
|
+
const name = unitSuffixOf(trimmed, units);
|
|
100
|
+
if (name !== void 0) {
|
|
101
|
+
const terms = compoundTermsOf(trimmed, units);
|
|
102
|
+
if (terms !== void 0 && terms.length > 1) return compoundValue(terms, trimmed, units);
|
|
103
|
+
const { factor, offset } = entryScale(units[name]);
|
|
104
|
+
return scaled(parseNumber(trimmed.slice(0, -name.length)), factor) + offset;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let t = text.trim().replace(MINUS_SIGN, "-");
|
|
108
|
+
const exponent = EXPONENTS[t.slice(-1).toLowerCase()];
|
|
109
|
+
if (exponent !== void 0) t = t.slice(0, -1).trimEnd();
|
|
110
|
+
if (/^[-+]?\d{1,3}(,\d{3})+(\.\d*)?$/.test(t)) t = t.replace(/,/g, "");
|
|
111
|
+
if (!/\d/.test(t)) return Number.NaN;
|
|
112
|
+
return Number(exponent === void 0 ? t : `${t}e${exponent}`);
|
|
113
|
+
}
|
|
114
|
+
function scaled(n, factor) {
|
|
115
|
+
return factor < 1 ? n / (1 / factor) : n * factor;
|
|
116
|
+
}
|
|
117
|
+
function compoundTermsOf(text, units) {
|
|
118
|
+
const names = [];
|
|
119
|
+
let rest = text.trimEnd();
|
|
120
|
+
while (rest !== "") {
|
|
121
|
+
const name = unitSuffixOf(rest, units);
|
|
122
|
+
if (name === void 0) return void 0;
|
|
123
|
+
names.push(name);
|
|
124
|
+
const before = rest.slice(0, -name.length);
|
|
125
|
+
const m = /(\d+(?:\.\d+)?|\.\d+)\s*$/.exec(before);
|
|
126
|
+
if (!m) return void 0;
|
|
127
|
+
rest = before.slice(0, m.index).trimEnd();
|
|
128
|
+
if (rest === "-" || rest === "+" || rest === MINUS_SIGN) rest = "";
|
|
129
|
+
}
|
|
130
|
+
return names.length === 0 ? void 0 : names.reverse();
|
|
131
|
+
}
|
|
132
|
+
function compoundValue(names, text, units) {
|
|
133
|
+
const digits = text.match(/\d+(?:\.\d+)?|\.\d+/g);
|
|
134
|
+
if (!digits || digits.length !== names.length) return Number.NaN;
|
|
135
|
+
const sign = /^\s*[-\u2212]/.test(text) ? -1 : 1;
|
|
136
|
+
let total = 0;
|
|
137
|
+
for (const [i, name] of names.entries()) {
|
|
138
|
+
const { factor, offset } = entryScale(units[name]);
|
|
139
|
+
if (offset !== 0) return Number.NaN;
|
|
140
|
+
total += scaled(Number(digits[i]), factor);
|
|
141
|
+
}
|
|
142
|
+
return sign * total;
|
|
143
|
+
}
|
|
144
|
+
function unitSuffixOf(text, units) {
|
|
145
|
+
const names = Object.keys(units).filter((n) => n !== "").sort((a, b) => b.length - a.length);
|
|
146
|
+
const lower = text.toLowerCase();
|
|
147
|
+
return names.find((n) => text.endsWith(n)) ?? names.find((n) => lower.endsWith(n.toLowerCase()));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/kind.ts
|
|
151
|
+
function textOf(parts) {
|
|
152
|
+
return parts.map((p) => p.value).join("");
|
|
153
|
+
}
|
|
154
|
+
function numberPart(value, locale, options) {
|
|
155
|
+
return { type: "number", value: signed(value.toLocaleString(locale, options)) };
|
|
156
|
+
}
|
|
157
|
+
function signed(text) {
|
|
158
|
+
return text.replace(/^-/, MINUS_SIGN);
|
|
159
|
+
}
|
|
160
|
+
function spokenSign(text) {
|
|
161
|
+
return text.replace(/^[-−]/, "minus ");
|
|
162
|
+
}
|
|
163
|
+
function plural(value, locale, singular, pluralForm) {
|
|
164
|
+
return new Intl.PluralRules(locale).select(value) === "one" ? singular : pluralForm;
|
|
165
|
+
}
|
|
166
|
+
function intlParts(value, locale, options) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const p of new Intl.NumberFormat(locale, options).formatToParts(value)) {
|
|
169
|
+
const type = DIGIT_RUNS.has(p.type) ? "number" : p.type;
|
|
170
|
+
const v = p.type === "minusSign" ? MINUS_SIGN : p.value;
|
|
171
|
+
const last = out[out.length - 1];
|
|
172
|
+
if (type === "number" && last?.type === "number") last.value += v;
|
|
173
|
+
else out.push({ type, value: v });
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
var DIGIT_RUNS = /* @__PURE__ */ new Set(["minusSign", "plusSign", "integer", "group", "decimal", "fraction"]);
|
|
178
|
+
|
|
179
|
+
// src/kinds/plain.ts
|
|
180
|
+
function decimal(options = {}) {
|
|
181
|
+
return { kind: "decimal", ...options };
|
|
182
|
+
}
|
|
183
|
+
var decimalKind = {
|
|
184
|
+
kind: "decimal",
|
|
185
|
+
format: (value, d, ctx) => intlParts(value, ctx.locale, decimalOptions(d)),
|
|
186
|
+
speak: (value, d, ctx) => spokenSign(textOf(intlParts(value, ctx.locale, decimalOptions(d)))),
|
|
187
|
+
parse: (text) => parseNumber(text)
|
|
188
|
+
};
|
|
189
|
+
function decimalOptions(d) {
|
|
190
|
+
const grouping = d.grouping ?? true;
|
|
191
|
+
return d.places === void 0 ? { maximumFractionDigits: d.maxPlaces ?? 3, useGrouping: grouping } : { minimumFractionDigits: d.places, maximumFractionDigits: d.places, useGrouping: grouping };
|
|
192
|
+
}
|
|
193
|
+
function integer(options = {}) {
|
|
194
|
+
return { kind: "integer", ...options };
|
|
195
|
+
}
|
|
196
|
+
var integerKind = {
|
|
197
|
+
kind: "integer",
|
|
198
|
+
format: (value, d, ctx) => intlParts(value, ctx.locale, { maximumFractionDigits: 0, useGrouping: d.grouping ?? true }),
|
|
199
|
+
speak: (value, d, ctx) => spokenSign(textOf(integerKind.format(value, d, ctx))),
|
|
200
|
+
parse: (text) => Math.round(parseNumber(text))
|
|
201
|
+
};
|
|
202
|
+
function compact(options = {}) {
|
|
203
|
+
return { kind: "compact", ...options };
|
|
204
|
+
}
|
|
205
|
+
var compactKind = {
|
|
206
|
+
kind: "compact",
|
|
207
|
+
format: (value, d) => {
|
|
208
|
+
const text = formatCompact(value, d.places ?? 0);
|
|
209
|
+
const m = /^(.*?\d)([A-Za-z]+)$/.exec(text);
|
|
210
|
+
return m ? [
|
|
211
|
+
{ type: "number", value: m[1] },
|
|
212
|
+
{ type: "compact", value: m[2] }
|
|
213
|
+
] : [{ type: "number", value: text }];
|
|
214
|
+
},
|
|
215
|
+
speak: (value, d, ctx) => spokenSign(
|
|
216
|
+
textOf(
|
|
217
|
+
Math.abs(value) < 1e3 ? compactKind.format(value, d, ctx) : intlParts(value, ctx.locale, {
|
|
218
|
+
notation: "compact",
|
|
219
|
+
compactDisplay: "long",
|
|
220
|
+
maximumSignificantDigits: 3
|
|
221
|
+
})
|
|
222
|
+
)
|
|
223
|
+
),
|
|
224
|
+
parse: (text) => parseNumber(text)
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/kinds/proportion.ts
|
|
228
|
+
function percent(options = {}) {
|
|
229
|
+
return { kind: "percent", ...options };
|
|
230
|
+
}
|
|
231
|
+
var percentKind = {
|
|
232
|
+
kind: "percent",
|
|
233
|
+
format: (value, d, ctx) => intlParts(value, ctx.locale, { style: "percent", maximumFractionDigits: d.places ?? 0 }),
|
|
234
|
+
speak: (value, d, ctx) => spokenSign(
|
|
235
|
+
textOf(
|
|
236
|
+
intlParts(value * 100, ctx.locale, {
|
|
237
|
+
style: "unit",
|
|
238
|
+
unit: "percent",
|
|
239
|
+
unitDisplay: "long",
|
|
240
|
+
maximumFractionDigits: d.places ?? 0
|
|
241
|
+
})
|
|
242
|
+
)
|
|
243
|
+
),
|
|
244
|
+
parse: (text) => parseNumber(text.replace(/%\s*$/, "")) / 100
|
|
245
|
+
};
|
|
246
|
+
function nearestFraction(x, maxDenominator) {
|
|
247
|
+
if (!Number.isFinite(x)) return [x, 1];
|
|
248
|
+
const sign = x < 0 ? -1 : 1;
|
|
249
|
+
const target = Math.abs(x);
|
|
250
|
+
let [h0, h1, k0, k1] = [0, 1, 1, 0];
|
|
251
|
+
let y = target;
|
|
252
|
+
for (let i = 0; i < 64; i++) {
|
|
253
|
+
const a = Math.floor(y);
|
|
254
|
+
const k2 = a * k1 + k0;
|
|
255
|
+
if (k2 > maxDenominator) {
|
|
256
|
+
const t = Math.floor((maxDenominator - k0) / k1);
|
|
257
|
+
const [hs, ks] = [t * h1 + h0, t * k1 + k0];
|
|
258
|
+
if (Math.abs(hs / ks - target) < Math.abs(h1 / k1 - target)) [h1, k1] = [hs, ks];
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
[h0, h1, k0, k1] = [h1, a * h1 + h0, k1, k2];
|
|
262
|
+
const rest = y - a;
|
|
263
|
+
if (rest < 1e-12 || Math.abs(h1 / k1 - target) < 1e-15) break;
|
|
264
|
+
y = 1 / rest;
|
|
265
|
+
}
|
|
266
|
+
return [sign * h1, k1];
|
|
267
|
+
}
|
|
268
|
+
function fraction(options = {}) {
|
|
269
|
+
return { kind: "fraction", ...options };
|
|
270
|
+
}
|
|
271
|
+
function fractionPieces(value, d) {
|
|
272
|
+
const [n, den] = nearestFraction(value, d.maxDenominator ?? 64);
|
|
273
|
+
const negative = n < 0;
|
|
274
|
+
const abs = Math.abs(n);
|
|
275
|
+
const whole = d.mixed || den === 1 ? Math.floor(abs / den) : 0;
|
|
276
|
+
return { negative, whole, num: abs - whole * den, den };
|
|
277
|
+
}
|
|
278
|
+
var fractionKind = {
|
|
279
|
+
kind: "fraction",
|
|
280
|
+
format: (value, d) => {
|
|
281
|
+
if (!Number.isFinite(value)) return [{ type: "number", value: String(value) }];
|
|
282
|
+
const { negative, whole, num, den } = fractionPieces(value, d);
|
|
283
|
+
const parts = [];
|
|
284
|
+
if (negative) parts.push({ type: "sign", value: MINUS_SIGN });
|
|
285
|
+
if (whole > 0 || num === 0) parts.push({ type: "whole", value: String(whole) });
|
|
286
|
+
if (num > 0) {
|
|
287
|
+
if (whole > 0) parts.push({ type: "literal", value: " " });
|
|
288
|
+
parts.push(
|
|
289
|
+
{ type: "numerator", value: String(num) },
|
|
290
|
+
{ type: "literal", value: "/" },
|
|
291
|
+
{ type: "denominator", value: String(den) }
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return parts;
|
|
295
|
+
},
|
|
296
|
+
speak: (value, d) => {
|
|
297
|
+
if (!Number.isFinite(value)) return String(value);
|
|
298
|
+
const { negative, whole, num, den } = fractionPieces(value, d);
|
|
299
|
+
const words = [
|
|
300
|
+
whole > 0 || num === 0 ? String(whole) : "",
|
|
301
|
+
whole > 0 && num > 0 ? "and" : "",
|
|
302
|
+
num > 0 ? `${num} over ${den}` : ""
|
|
303
|
+
].filter(Boolean);
|
|
304
|
+
return `${negative ? "minus " : ""}${words.join(" ")}`;
|
|
305
|
+
},
|
|
306
|
+
parse: (text) => {
|
|
307
|
+
const m = /^\s*([-−+]?)\s*(?:(\d+)\s+)?(\d+)\s*[/⁄]\s*(\d+)\s*$/.exec(text);
|
|
308
|
+
if (!m) return parseNumber(text);
|
|
309
|
+
const magnitude = Number(m[2] ?? 0) + Number(m[3]) / Number(m[4]);
|
|
310
|
+
return m[1] === "-" || m[1] === MINUS_SIGN ? -magnitude : magnitude;
|
|
311
|
+
},
|
|
312
|
+
mathml: (value, d) => {
|
|
313
|
+
if (!Number.isFinite(value)) return void 0;
|
|
314
|
+
const { negative, whole, num, den } = fractionPieces(value, d);
|
|
315
|
+
const body = [
|
|
316
|
+
negative ? `<mo>${MINUS_SIGN}</mo>` : "",
|
|
317
|
+
whole > 0 || num === 0 ? `<mn>${whole}</mn>` : "",
|
|
318
|
+
num > 0 ? `<mfrac><mn>${num}</mn><mn>${den}</mn></mfrac>` : ""
|
|
319
|
+
].join("");
|
|
320
|
+
return `<math>${body}</math>`;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
function ratio(options = {}) {
|
|
324
|
+
return { kind: "ratio", ...options };
|
|
325
|
+
}
|
|
326
|
+
var ratioKind = {
|
|
327
|
+
kind: "ratio",
|
|
328
|
+
format: (value, d) => {
|
|
329
|
+
if (!Number.isFinite(value)) return [{ type: "number", value: String(value) }];
|
|
330
|
+
const [n, den] = nearestFraction(value, d.maxDenominator ?? 64);
|
|
331
|
+
return [
|
|
332
|
+
{ type: "antecedent", value: String(n).replace(/^-/, MINUS_SIGN) },
|
|
333
|
+
{ type: "literal", value: ":" },
|
|
334
|
+
{ type: "consequent", value: String(den) }
|
|
335
|
+
];
|
|
336
|
+
},
|
|
337
|
+
speak: (value, d) => {
|
|
338
|
+
if (!Number.isFinite(value)) return String(value);
|
|
339
|
+
const [n, den] = nearestFraction(value, d.maxDenominator ?? 64);
|
|
340
|
+
return spokenSign(`${String(n).replace(/^-/, MINUS_SIGN)} to ${den}`);
|
|
341
|
+
},
|
|
342
|
+
parse: (text) => {
|
|
343
|
+
const m = /^\s*(.+?)\s*:\s*(.+?)\s*$/.exec(text);
|
|
344
|
+
return m ? parseNumber(m[1]) / parseNumber(m[2]) : parseNumber(text);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function multiplier(options = {}) {
|
|
348
|
+
return { kind: "multiplier", ...options };
|
|
349
|
+
}
|
|
350
|
+
var multiplierKind = {
|
|
351
|
+
kind: "multiplier",
|
|
352
|
+
format: (value, d, ctx) => [
|
|
353
|
+
numberPart(value, ctx.locale, { maximumFractionDigits: d.places ?? 2 }),
|
|
354
|
+
{ type: "symbol", value: d.symbol ?? "\xD7" }
|
|
355
|
+
],
|
|
356
|
+
speak: (value, d, ctx) => spokenSign(`${numberPart(value, ctx.locale, { maximumFractionDigits: d.places ?? 2 }).value} times`),
|
|
357
|
+
parse: (text) => parseNumber(text.replace(/\s*[x×X]\s*$/, ""))
|
|
358
|
+
};
|
|
359
|
+
function zoom() {
|
|
360
|
+
return { kind: "zoom" };
|
|
361
|
+
}
|
|
362
|
+
function zoomParts(z) {
|
|
363
|
+
if (!Number.isFinite(z)) return [{ type: "number", value: String(z) }];
|
|
364
|
+
const n = (v, grouping = false) => ({ type: "number", value: v.toLocaleString("en-US", { useGrouping: grouping, maximumFractionDigits: 1 }).replace(/^-/, MINUS_SIGN) });
|
|
365
|
+
if (z <= 2) return [n(Math.round(z * 100)), { type: "percentSign", value: "%" }];
|
|
366
|
+
if (z >= 100) return [n(Math.round(z), true), { type: "symbol", value: "x" }];
|
|
367
|
+
return [n(Math.round(z * 10) / 10), { type: "symbol", value: "x" }];
|
|
368
|
+
}
|
|
369
|
+
var zoomKind = {
|
|
370
|
+
kind: "zoom",
|
|
371
|
+
format: (value) => zoomParts(value),
|
|
372
|
+
speak: (value) => spokenSign(
|
|
373
|
+
zoomParts(value).map((p) => p.type === "percentSign" ? " percent" : p.type === "symbol" ? " times" : p.value).join("")
|
|
374
|
+
),
|
|
375
|
+
parse: (text) => {
|
|
376
|
+
const t = text.trim();
|
|
377
|
+
if (/[x×X]$/.test(t)) return parseNumber(t.slice(0, -1));
|
|
378
|
+
return parseNumber(t.replace(/%$/, "")) / 100;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/kinds/measure.ts
|
|
383
|
+
var INTL_UNITS = {
|
|
384
|
+
mm: "millimeter",
|
|
385
|
+
cm: "centimeter",
|
|
386
|
+
m: "meter",
|
|
387
|
+
km: "kilometer",
|
|
388
|
+
in: "inch",
|
|
389
|
+
ft: "foot",
|
|
390
|
+
yd: "yard",
|
|
391
|
+
mi: "mile",
|
|
392
|
+
deg: "degree",
|
|
393
|
+
"\xB0": "degree",
|
|
394
|
+
ms: "millisecond",
|
|
395
|
+
s: "second",
|
|
396
|
+
min: "minute",
|
|
397
|
+
h: "hour",
|
|
398
|
+
g: "gram",
|
|
399
|
+
kg: "kilogram",
|
|
400
|
+
L: "liter",
|
|
401
|
+
"%": "percent"
|
|
402
|
+
};
|
|
403
|
+
var OWN_UNITS = {
|
|
404
|
+
px: ["pixel", "pixels"],
|
|
405
|
+
pt: ["point", "points"],
|
|
406
|
+
rad: ["radian", "radians"],
|
|
407
|
+
turn: ["turn", "turns"]
|
|
408
|
+
};
|
|
409
|
+
function unit(name, options = {}) {
|
|
410
|
+
return name === void 0 ? { kind: "unit", ...options } : { kind: "unit", unit: name, ...options };
|
|
411
|
+
}
|
|
412
|
+
var unitKind = {
|
|
413
|
+
kind: "unit",
|
|
414
|
+
format: (value, d, ctx) => {
|
|
415
|
+
const parts = [numberPart(value, ctx.locale, { maximumFractionDigits: d.places ?? 2, useGrouping: false })];
|
|
416
|
+
const name = d.unit ?? ctx.unit;
|
|
417
|
+
if (name) {
|
|
418
|
+
if (d.space) parts.push({ type: "literal", value: " " });
|
|
419
|
+
parts.push({ type: "unit", value: name });
|
|
420
|
+
}
|
|
421
|
+
return parts;
|
|
422
|
+
},
|
|
423
|
+
speak: (value, d, ctx) => {
|
|
424
|
+
const places = d.places ?? 2;
|
|
425
|
+
const name = d.unit ?? ctx.unit;
|
|
426
|
+
const intl = name === void 0 ? void 0 : INTL_UNITS[name];
|
|
427
|
+
if (intl !== void 0 && d.spoken === void 0) {
|
|
428
|
+
return spokenSign(
|
|
429
|
+
textOf(intlParts(value, ctx.locale, { style: "unit", unit: intl, unitDisplay: "long", maximumFractionDigits: places }))
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const number = numberPart(value, ctx.locale, { maximumFractionDigits: places, useGrouping: false }).value;
|
|
433
|
+
const names = d.spoken ?? (name === void 0 ? void 0 : OWN_UNITS[name]);
|
|
434
|
+
const word = names ? plural(value, ctx.locale, names[0], names[1]) : name;
|
|
435
|
+
return spokenSign(word ? `${number} ${word}` : number);
|
|
436
|
+
},
|
|
437
|
+
parse: (text, d, ctx) => {
|
|
438
|
+
const name = d.unit ?? ctx.unit;
|
|
439
|
+
return parseNumber(text, d.accepts ?? (name ? { [name]: 1 } : void 0));
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
function currency(code, options = {}) {
|
|
443
|
+
return { kind: "currency", currency: code, ...options };
|
|
444
|
+
}
|
|
445
|
+
function currencyOptions(d) {
|
|
446
|
+
const options = { style: "currency", currency: d.currency };
|
|
447
|
+
if (d.places !== void 0) {
|
|
448
|
+
options.minimumFractionDigits = d.places;
|
|
449
|
+
options.maximumFractionDigits = d.places;
|
|
450
|
+
}
|
|
451
|
+
return options;
|
|
452
|
+
}
|
|
453
|
+
var currencyKind = {
|
|
454
|
+
kind: "currency",
|
|
455
|
+
format: (value, d, ctx) => intlParts(value, ctx.locale, currencyOptions(d)),
|
|
456
|
+
speak: (value, d, ctx) => spokenSign(textOf(intlParts(value, ctx.locale, { ...currencyOptions(d), currencyDisplay: "name" }))),
|
|
457
|
+
parse: (text) => parseNumber(text.replace(/[^\d.,+\-−]/g, ""))
|
|
458
|
+
};
|
|
459
|
+
function duration(options = {}) {
|
|
460
|
+
return { kind: "duration", ...options };
|
|
461
|
+
}
|
|
462
|
+
function hms(value, places) {
|
|
463
|
+
const scale = 10 ** places;
|
|
464
|
+
const total = Math.round(Math.abs(value) * scale) / scale;
|
|
465
|
+
const h = Math.floor(total / 3600);
|
|
466
|
+
const m = Math.floor((total - h * 3600) / 60);
|
|
467
|
+
const s = Math.round((total - h * 3600 - m * 60) * scale) / scale;
|
|
468
|
+
return { negative: value < 0 && total > 0, h, m, s };
|
|
469
|
+
}
|
|
470
|
+
var DURATION_UNITS = { h: 3600, m: 60, min: 60, s: 1, ms: 1e-3 };
|
|
471
|
+
var durationKind = {
|
|
472
|
+
kind: "duration",
|
|
473
|
+
format: (value, d) => {
|
|
474
|
+
if (!Number.isFinite(value)) return [{ type: "number", value: String(value) }];
|
|
475
|
+
const places = d.places ?? 0;
|
|
476
|
+
const { negative, h, m, s } = hms(value, places);
|
|
477
|
+
const sec = s.toFixed(places);
|
|
478
|
+
const parts = negative ? [{ type: "sign", value: "\u2212" }] : [];
|
|
479
|
+
if (d.style === "units") {
|
|
480
|
+
const terms = [];
|
|
481
|
+
if (h > 0) terms.push([{ type: "hours", value: String(h) }, { type: "unit", value: "h" }]);
|
|
482
|
+
if (m > 0) terms.push([{ type: "minutes", value: String(m) }, { type: "unit", value: "m" }]);
|
|
483
|
+
if (s > 0 || terms.length === 0) terms.push([{ type: "seconds", value: sec }, { type: "unit", value: "s" }]);
|
|
484
|
+
terms.forEach((t, i) => parts.push(...i > 0 ? [{ type: "literal", value: " " }] : [], ...t));
|
|
485
|
+
return parts;
|
|
486
|
+
}
|
|
487
|
+
const pad = (n) => n.split(".")[0].length < 2 ? `0${n}` : n;
|
|
488
|
+
if (h > 0) {
|
|
489
|
+
parts.push(
|
|
490
|
+
{ type: "hours", value: String(h) },
|
|
491
|
+
{ type: "literal", value: ":" },
|
|
492
|
+
{ type: "minutes", value: pad(String(m)) }
|
|
493
|
+
);
|
|
494
|
+
} else {
|
|
495
|
+
parts.push({ type: "minutes", value: String(m) });
|
|
496
|
+
}
|
|
497
|
+
parts.push({ type: "literal", value: ":" }, { type: "seconds", value: pad(sec) });
|
|
498
|
+
return parts;
|
|
499
|
+
},
|
|
500
|
+
speak: (value, d, ctx) => {
|
|
501
|
+
if (!Number.isFinite(value)) return String(value);
|
|
502
|
+
const { negative, h, m, s } = hms(value, d.places ?? 0);
|
|
503
|
+
const terms = [];
|
|
504
|
+
if (h > 0) terms.push(`${h} ${plural(h, ctx.locale, "hour", "hours")}`);
|
|
505
|
+
if (m > 0) terms.push(`${m} ${plural(m, ctx.locale, "minute", "minutes")}`);
|
|
506
|
+
if (s > 0 || terms.length === 0) terms.push(`${s} ${plural(s, ctx.locale, "second", "seconds")}`);
|
|
507
|
+
return `${negative ? "minus " : ""}${terms.join(", ")}`;
|
|
508
|
+
},
|
|
509
|
+
parse: (text) => {
|
|
510
|
+
const m = /^\s*([-−+]?)\s*(?:(\d+):)?(\d+):(\d+(?:\.\d+)?)\s*$/.exec(text);
|
|
511
|
+
if (!m) return parseNumber(text, DURATION_UNITS);
|
|
512
|
+
const total = Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
|
|
513
|
+
return m[1] === "-" || m[1] === "\u2212" ? -total : total;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
var DECIMAL_BYTES = ["B", "kB", "MB", "GB", "TB", "PB"];
|
|
517
|
+
var BINARY_BYTES = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
|
518
|
+
var DECIMAL_NAMES = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte"];
|
|
519
|
+
var BINARY_NAMES = ["byte", "kibibyte", "mebibyte", "gibibyte", "tebibyte", "pebibyte"];
|
|
520
|
+
function bytes(options = {}) {
|
|
521
|
+
return { kind: "bytes", ...options };
|
|
522
|
+
}
|
|
523
|
+
function byteScale(value, base) {
|
|
524
|
+
let step = 0;
|
|
525
|
+
let scaled2 = value;
|
|
526
|
+
while (Math.abs(scaled2) >= base && step < DECIMAL_BYTES.length - 1) {
|
|
527
|
+
scaled2 /= base;
|
|
528
|
+
step++;
|
|
529
|
+
}
|
|
530
|
+
return { scaled: scaled2, step };
|
|
531
|
+
}
|
|
532
|
+
var BYTE_UNITS = Object.fromEntries([
|
|
533
|
+
...DECIMAL_BYTES.map((u, i) => [u, 1e3 ** i]),
|
|
534
|
+
["KB", 1e3],
|
|
535
|
+
...BINARY_BYTES.slice(1).map((u, i) => [u, 1024 ** (i + 1)])
|
|
536
|
+
]);
|
|
537
|
+
var bytesKind = {
|
|
538
|
+
kind: "bytes",
|
|
539
|
+
format: (value, d, ctx) => {
|
|
540
|
+
const base = d.base ?? 1e3;
|
|
541
|
+
const { scaled: scaled2, step } = byteScale(value, base);
|
|
542
|
+
const units = base === 1024 ? BINARY_BYTES : DECIMAL_BYTES;
|
|
543
|
+
return [
|
|
544
|
+
numberPart(scaled2, ctx.locale, { maximumFractionDigits: step === 0 ? 0 : d.places ?? 1 }),
|
|
545
|
+
{ type: "literal", value: " " },
|
|
546
|
+
{ type: "unit", value: units[step] }
|
|
547
|
+
];
|
|
548
|
+
},
|
|
549
|
+
speak: (value, d, ctx) => {
|
|
550
|
+
const base = d.base ?? 1e3;
|
|
551
|
+
const { scaled: scaled2, step } = byteScale(value, base);
|
|
552
|
+
const name = (base === 1024 ? BINARY_NAMES : DECIMAL_NAMES)[step];
|
|
553
|
+
const number = numberPart(scaled2, ctx.locale, { maximumFractionDigits: step === 0 ? 0 : d.places ?? 1 }).value;
|
|
554
|
+
return spokenSign(`${number} ${plural(Number(number.replace(/[^\d.]/g, "")), ctx.locale, name, `${name}s`)}`);
|
|
555
|
+
},
|
|
556
|
+
parse: (text) => parseNumber(text, BYTE_UNITS)
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/kinds/numeral.ts
|
|
560
|
+
function roman(options = {}) {
|
|
561
|
+
return { kind: "roman", ...options };
|
|
562
|
+
}
|
|
563
|
+
var NUMERALS = [
|
|
564
|
+
[1e3, "M"],
|
|
565
|
+
[900, "CM"],
|
|
566
|
+
[500, "D"],
|
|
567
|
+
[400, "CD"],
|
|
568
|
+
[100, "C"],
|
|
569
|
+
[90, "XC"],
|
|
570
|
+
[50, "L"],
|
|
571
|
+
[40, "XL"],
|
|
572
|
+
[10, "X"],
|
|
573
|
+
[9, "IX"],
|
|
574
|
+
[5, "V"],
|
|
575
|
+
[4, "IV"],
|
|
576
|
+
[1, "I"]
|
|
577
|
+
];
|
|
578
|
+
function toRoman(n) {
|
|
579
|
+
let rest = Math.round(n);
|
|
580
|
+
if (!(rest >= 1 && rest <= 3999)) return void 0;
|
|
581
|
+
let out = "";
|
|
582
|
+
for (const [v, s] of NUMERALS) {
|
|
583
|
+
while (rest >= v) {
|
|
584
|
+
out += s;
|
|
585
|
+
rest -= v;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
function fromRoman(text) {
|
|
591
|
+
const t = text.trim().toUpperCase();
|
|
592
|
+
if (!/^[MDCLXVI]+$/.test(t)) return Number.NaN;
|
|
593
|
+
let total = 0;
|
|
594
|
+
let i = 0;
|
|
595
|
+
for (const [v, s] of NUMERALS) {
|
|
596
|
+
while (t.startsWith(s, i)) {
|
|
597
|
+
total += v;
|
|
598
|
+
i += s.length;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return i === t.length && toRoman(total) === t ? total : Number.NaN;
|
|
602
|
+
}
|
|
603
|
+
var romanKind = {
|
|
604
|
+
kind: "roman",
|
|
605
|
+
format: (value, d, ctx) => {
|
|
606
|
+
const r = toRoman(value);
|
|
607
|
+
if (r === void 0) return [numberPart(value, ctx.locale, { maximumFractionDigits: 0 })];
|
|
608
|
+
return [{ type: "numeral", value: d.lower ? r.toLowerCase() : r }];
|
|
609
|
+
},
|
|
610
|
+
speak: (value, _d, ctx) => spokenSign(numberPart(Math.round(value), ctx.locale, { maximumFractionDigits: 0, useGrouping: false }).value),
|
|
611
|
+
parse: (text) => {
|
|
612
|
+
const r = fromRoman(text);
|
|
613
|
+
return Number.isNaN(r) ? parseNumber(text) : r;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function ordinal() {
|
|
617
|
+
return { kind: "ordinal" };
|
|
618
|
+
}
|
|
619
|
+
var SUFFIXES = { one: "st", two: "nd", few: "rd", other: "th" };
|
|
620
|
+
var ordinalKind = {
|
|
621
|
+
kind: "ordinal",
|
|
622
|
+
format: (value, _d, ctx) => {
|
|
623
|
+
const n = Math.round(value);
|
|
624
|
+
const number = numberPart(n, ctx.locale, { maximumFractionDigits: 0 });
|
|
625
|
+
if (!Number.isFinite(n) || !ctx.locale.startsWith("en")) return [number];
|
|
626
|
+
const rule = new Intl.PluralRules("en-US", { type: "ordinal" }).select(n);
|
|
627
|
+
return [number, { type: "ordinal", value: SUFFIXES[rule] ?? "th" }];
|
|
628
|
+
},
|
|
629
|
+
parse: (text) => Math.round(parseNumber(text.replace(/(st|nd|rd|th)\s*$/i, "")))
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/present.ts
|
|
633
|
+
var BUILTIN = Object.fromEntries(
|
|
634
|
+
[
|
|
635
|
+
decimalKind,
|
|
636
|
+
integerKind,
|
|
637
|
+
compactKind,
|
|
638
|
+
percentKind,
|
|
639
|
+
fractionKind,
|
|
640
|
+
ratioKind,
|
|
641
|
+
multiplierKind,
|
|
642
|
+
zoomKind,
|
|
643
|
+
unitKind,
|
|
644
|
+
currencyKind,
|
|
645
|
+
durationKind,
|
|
646
|
+
bytesKind,
|
|
647
|
+
romanKind,
|
|
648
|
+
ordinalKind
|
|
649
|
+
].map((k) => [k.kind, k])
|
|
650
|
+
);
|
|
651
|
+
var registered = /* @__PURE__ */ new Map();
|
|
652
|
+
function registerDisplayKind(kind) {
|
|
653
|
+
const entry = kind;
|
|
654
|
+
registered.set(kind.kind, entry);
|
|
655
|
+
return () => {
|
|
656
|
+
if (registered.get(kind.kind) === entry) registered.delete(kind.kind);
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function displayKindOf(display) {
|
|
660
|
+
const kind = display === void 0 ? void 0 : registered.get(display.kind) ?? BUILTIN[display.kind];
|
|
661
|
+
return kind ?? decimalKind;
|
|
662
|
+
}
|
|
663
|
+
var DEFAULT_DISPLAY = { kind: "decimal" };
|
|
664
|
+
function contextOf(q, options) {
|
|
665
|
+
const unit2 = unitOf(q);
|
|
666
|
+
return unit2 === void 0 ? { locale: options?.locale ?? "en-US" } : { locale: options?.locale ?? "en-US", unit: unit2 };
|
|
667
|
+
}
|
|
668
|
+
var ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
669
|
+
function escape(s) {
|
|
670
|
+
return s.replace(/[&<>"]/g, (c) => ESCAPES[c]);
|
|
671
|
+
}
|
|
672
|
+
function partsToHtml(value, parts) {
|
|
673
|
+
const body = parts.map((p) => p.type === "literal" ? escape(p.value) : `<span data-part="${escape(p.type)}">${escape(p.value)}</span>`).join("");
|
|
674
|
+
return `<data value="${escape(String(value))}">${body}</data>`;
|
|
675
|
+
}
|
|
676
|
+
function partsOf(q, fallback, options) {
|
|
677
|
+
const display = displayOf(q, fallback) ?? DEFAULT_DISPLAY;
|
|
678
|
+
return displayKindOf(display).format(amount(q), display, contextOf(q, options));
|
|
679
|
+
}
|
|
680
|
+
function present(q, fallback, options) {
|
|
681
|
+
const display = displayOf(q, fallback) ?? DEFAULT_DISPLAY;
|
|
682
|
+
const kind = displayKindOf(display);
|
|
683
|
+
const value = amount(q);
|
|
684
|
+
const ctx = contextOf(q, options);
|
|
685
|
+
const parts = kind.format(value, display, ctx);
|
|
686
|
+
const text = textOf(parts);
|
|
687
|
+
const out = {
|
|
688
|
+
text,
|
|
689
|
+
spoken: kind.speak ? kind.speak(value, display, ctx) : text,
|
|
690
|
+
html: partsToHtml(value, parts)
|
|
691
|
+
};
|
|
692
|
+
const mathml = kind.mathml?.(value, display, ctx);
|
|
693
|
+
if (mathml !== void 0) out.mathml = mathml;
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
function parseAs(text, display, options) {
|
|
697
|
+
const d = display ?? DEFAULT_DISPLAY;
|
|
698
|
+
const kind = displayKindOf(d);
|
|
699
|
+
const ctx = { locale: options?.locale ?? "en-US" };
|
|
700
|
+
if (options?.unit !== void 0) ctx.unit = options.unit;
|
|
701
|
+
return kind.parse ? kind.parse(text, d, ctx) : parseNumber(text);
|
|
702
|
+
}
|
|
703
|
+
var QuantityView = class _QuantityView {
|
|
704
|
+
constructor(raw, fallback, options) {
|
|
705
|
+
this.raw = raw;
|
|
706
|
+
this.fallback = fallback;
|
|
707
|
+
this.options = options;
|
|
708
|
+
}
|
|
709
|
+
raw;
|
|
710
|
+
fallback;
|
|
711
|
+
options;
|
|
712
|
+
get value() {
|
|
713
|
+
return amount(this.raw);
|
|
714
|
+
}
|
|
715
|
+
get unit() {
|
|
716
|
+
return unitOf(this.raw);
|
|
717
|
+
}
|
|
718
|
+
/** The display in effect: the value's own, else the fallback. */
|
|
719
|
+
get display() {
|
|
720
|
+
return displayOf(this.raw, this.fallback);
|
|
721
|
+
}
|
|
722
|
+
get parts() {
|
|
723
|
+
return partsOf(this.raw, this.fallback, this.options);
|
|
724
|
+
}
|
|
725
|
+
get text() {
|
|
726
|
+
return textOf(this.parts);
|
|
727
|
+
}
|
|
728
|
+
get spoken() {
|
|
729
|
+
return present(this.raw, this.fallback, this.options).spoken;
|
|
730
|
+
}
|
|
731
|
+
get html() {
|
|
732
|
+
return partsToHtml(this.value, this.parts);
|
|
733
|
+
}
|
|
734
|
+
get mathml() {
|
|
735
|
+
return present(this.raw, this.fallback, this.options).mathml;
|
|
736
|
+
}
|
|
737
|
+
/** `next` in this quantity's shape — see {@link retag}. */
|
|
738
|
+
with(next) {
|
|
739
|
+
return new _QuantityView(retag(this.raw, next), this.fallback, this.options);
|
|
740
|
+
}
|
|
741
|
+
/** Typed text read through this quantity's display; NaN when it does not read. */
|
|
742
|
+
parse(text) {
|
|
743
|
+
const unit2 = this.unit;
|
|
744
|
+
return parseAs(text, this.display, unit2 === void 0 ? this.options : { ...this.options, unit: unit2 });
|
|
745
|
+
}
|
|
746
|
+
/** The same amount in `target`, converted through `system`, tagged with it. */
|
|
747
|
+
to(target, system) {
|
|
748
|
+
const from = unitScale(system, this.unit ?? system.base);
|
|
749
|
+
const to = unitScale(system, target);
|
|
750
|
+
const base = this.value * from.factor + from.offset;
|
|
751
|
+
const value = (base - to.offset) / to.factor;
|
|
752
|
+
const raw = typeof this.raw === "number" ? tag(value, void 0, target) : { ...this.raw, value, unit: target };
|
|
753
|
+
return new _QuantityView(raw, this.fallback, this.options);
|
|
754
|
+
}
|
|
755
|
+
toJSON() {
|
|
756
|
+
return this.raw;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
function qty(q, fallback, options) {
|
|
760
|
+
return new QuantityView(q, fallback, options);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export { ANGLE_RADIANS, IMPERIAL_INCHES, METRIC_MM, MINUS_SIGN, PIXELS, QuantityView, amount, bytes, bytesKind, compact, compactKind, currency, currencyKind, decimal, decimalKind, displayKindOf, displayOf, duration, durationKind, entryScale, formatCompact, formatNumber, formatUnit, fraction, fractionKind, fromRoman, integer, integerKind, intlParts, multiplier, multiplierKind, nearestFraction, numberPart, ordinal, ordinalKind, parseAs, parseNumber, parseSignedNumber, partsOf, partsToHtml, percent, percentKind, plural, present, qty, ratio, ratioKind, registerDisplayKind, resolveUnit, retag, roman, romanKind, signed, spokenSign, tag, textOf, toRoman, unit, unitKind, unitOf, unitScale, zoom, zoomKind };
|
|
764
|
+
//# sourceMappingURL=index.js.map
|
|
765
|
+
//# sourceMappingURL=index.js.map
|