@wzo/calc 0.0.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/dist/index.cjs ADDED
@@ -0,0 +1,1354 @@
1
+
2
+ //#region src/utils/config.ts
3
+ /** Default configuration values. */
4
+ const DEFAULT_CONFIG = {
5
+ _error: "-",
6
+ _fmt: void 0,
7
+ _precision: 50
8
+ };
9
+ const config = { ...DEFAULT_CONFIG };
10
+ /**
11
+ * Partially updates the global configuration (only the provided fields are overwritten).
12
+ *
13
+ * @param patch Configuration fields to update
14
+ * @example
15
+ * setConfig({ _precision: 10, _fmt: { decimals: 2 } })
16
+ */
17
+ const setConfig = (patch) => {
18
+ Object.assign(config, patch);
19
+ };
20
+ /**
21
+ * Resets the global configuration to its default values.
22
+ *
23
+ * @example
24
+ * resetConfig()
25
+ */
26
+ const resetConfig = () => {
27
+ Object.assign(config, DEFAULT_CONFIG);
28
+ };
29
+ /**
30
+ * Returns the current global configuration object (by reference — do not mutate directly; use {@link setConfig}).
31
+ *
32
+ * @returns The current {@link IGlobalConfig}
33
+ */
34
+ const getConfig = () => config;
35
+ /** Returns the global config; if `_precision` is provided, returns a copy with that field overridden without mutating the global singleton. */
36
+ const configWithPrecision = (opt) => opt?._precision != null ? {
37
+ ...config,
38
+ _precision: opt._precision
39
+ } : config;
40
+
41
+ //#endregion
42
+ //#region src/utils/precision.ts
43
+ const TEN = 10n;
44
+ const POW10 = [1n];
45
+ const pow10 = (n) => {
46
+ for (let i = POW10.length; i <= n; i++) POW10[i] = POW10[i - 1] * TEN;
47
+ return POW10[n];
48
+ };
49
+ const RE_SCI = /^([+-]?\d+(?:\.\d+)?)e([+-]?\d+)$/i;
50
+ const RE_NUM = /^\d+(?:\.\d+)?$/;
51
+ const RE_TRAIL_ZERO$1 = /0+$/;
52
+ /** Shifts the decimal point of `"1.23"` left or right by `n` positions → string. */
53
+ const shiftDecimalPoint = (s, n) => {
54
+ let sign = "";
55
+ if (s.startsWith("-")) {
56
+ sign = "-";
57
+ s = s.slice(1);
58
+ } else if (s.startsWith("+")) s = s.slice(1);
59
+ const dot = s.indexOf(".");
60
+ const allDigits = dot === -1 ? s : s.slice(0, dot) + s.slice(dot + 1);
61
+ const dotPos = (dot === -1 ? s.length : dot) + n;
62
+ if (dotPos <= 0) return `${sign}0.${"0".repeat(-dotPos)}${allDigits}`;
63
+ if (dotPos >= allDigits.length) return sign + allDigits + "0".repeat(dotPos - allDigits.length);
64
+ return `${sign}${allDigits.slice(0, dotPos)}.${allDigits.slice(dotPos)}`;
65
+ };
66
+ /**
67
+ * Parses a string, number, or bigint into the internal {@link IDecimal} representation.
68
+ *
69
+ * Supports standard decimals (`"1.23"`, `-0.5`) and scientific notation (`"1.2e-3"`).
70
+ *
71
+ * @param input Value to parse, e.g. `"3.14"`, `42`, `-1n`, `"1e10"`
72
+ * @returns Parsed {@link IDecimal} (`digits` is always >= 0; sign is carried by `sign`)
73
+ * @throws When the string is not a valid number
74
+ * @example
75
+ * parse('1.23') // { sign: 1, digits: 123n, exp: 2 }
76
+ * parse('-5') // { sign: -1, digits: 5n, exp: 0 }
77
+ */
78
+ const parse = (input) => {
79
+ if (typeof input === "bigint") return {
80
+ sign: input < 0n ? -1 : 1,
81
+ digits: input < 0n ? -input : input,
82
+ exp: 0
83
+ };
84
+ let s = String(input).trim();
85
+ if (s === "" || s === "-" || s === "+") throw new Error(`Cannot parse number: "${input}"`);
86
+ const expMatch = RE_SCI.exec(s);
87
+ if (expMatch) {
88
+ const base = expMatch[1];
89
+ s = shiftDecimalPoint(base, Number.parseInt(expMatch[2], 10));
90
+ }
91
+ let sign = 1;
92
+ if (s.startsWith("+")) s = s.slice(1);
93
+ else if (s.startsWith("-")) {
94
+ sign = -1;
95
+ s = s.slice(1);
96
+ }
97
+ if (!RE_NUM.test(s)) throw new Error(`Cannot parse number: "${input}"`);
98
+ const dot = s.indexOf(".");
99
+ const digitStr = dot === -1 ? s : s.slice(0, dot) + s.slice(dot + 1);
100
+ const exp = dot === -1 ? 0 : s.length - dot - 1;
101
+ const digits = BigInt(digitStr || "0");
102
+ if (digits === 0n) return {
103
+ sign: 1,
104
+ digits: 0n,
105
+ exp: 0
106
+ };
107
+ return {
108
+ sign,
109
+ digits,
110
+ exp
111
+ };
112
+ };
113
+ /**
114
+ * Converts an {@link IDecimal} back to a canonical decimal string (trailing zeros are stripped automatically).
115
+ *
116
+ * @param d The {@link IDecimal} to format
117
+ * @returns Canonical string; zero is always returned as `"0"`
118
+ * @example
119
+ * format({ sign: 1, digits: 1230n, exp: 3 }) // '1.23'
120
+ */
121
+ const format = (d) => {
122
+ if (d.digits === 0n) return "0";
123
+ let raw = d.digits.toString();
124
+ if (d.exp > 0) {
125
+ if (raw.length <= d.exp) raw = "0".repeat(d.exp - raw.length + 1) + raw;
126
+ const intPart = raw.slice(0, raw.length - d.exp);
127
+ const fracPart = raw.slice(raw.length - d.exp).replace(RE_TRAIL_ZERO$1, "");
128
+ raw = fracPart ? `${intPart}.${fracPart}` : intPart;
129
+ }
130
+ return (d.sign < 0 ? "-" : "") + raw;
131
+ };
132
+ /** Aligns the digits of two numbers to the same exp (using the larger of the two). */
133
+ const align = (a, b) => {
134
+ if (a.exp === b.exp) return {
135
+ aN: a.digits,
136
+ bN: b.digits,
137
+ exp: a.exp
138
+ };
139
+ if (a.exp > b.exp) return {
140
+ aN: a.digits,
141
+ bN: b.digits * TEN ** BigInt(a.exp - b.exp),
142
+ exp: a.exp
143
+ };
144
+ return {
145
+ aN: a.digits * TEN ** BigInt(b.exp - a.exp),
146
+ bN: b.digits,
147
+ exp: b.exp
148
+ };
149
+ };
150
+ const fromBig = (n, exp) => {
151
+ if (n === 0n) return {
152
+ sign: 1,
153
+ digits: 0n,
154
+ exp: 0
155
+ };
156
+ return {
157
+ sign: n < 0n ? -1 : 1,
158
+ digits: n < 0n ? -n : n,
159
+ exp
160
+ };
161
+ };
162
+ const MAX_SAFE = Number.MAX_SAFE_INTEGER;
163
+ const POW10F = [
164
+ 1,
165
+ 10,
166
+ 100,
167
+ 1e3,
168
+ 1e4,
169
+ 1e5,
170
+ 1e6,
171
+ 1e7,
172
+ 1e8,
173
+ 1e9,
174
+ 1e10,
175
+ 1e11,
176
+ 0xe8d4a51000,
177
+ 0x9184e72a000,
178
+ 0x5af3107a4000,
179
+ 0x38d7ea4c68000
180
+ ];
181
+ /** Parses input into `int / 10^scale` (int is a safe integer); returns `null` when safe representation is not possible, signalling the caller to fall back. */
182
+ const parseFast = (input) => {
183
+ const s = typeof input === "string" ? input : String(input);
184
+ let i = 0;
185
+ let neg$1 = false;
186
+ const c0 = s.charCodeAt(0);
187
+ if (c0 === 45) {
188
+ neg$1 = true;
189
+ i = 1;
190
+ } else if (c0 === 43) i = 1;
191
+ let hasDot = false;
192
+ let scale = 0;
193
+ let digits = "";
194
+ for (; i < s.length; i++) {
195
+ const c = s.charCodeAt(i);
196
+ if (c === 46) {
197
+ if (hasDot || digits === "") return null;
198
+ hasDot = true;
199
+ } else if (c >= 48 && c <= 57) {
200
+ digits += s[i];
201
+ if (hasDot) scale++;
202
+ } else return null;
203
+ }
204
+ if (digits === "" || hasDot && scale === 0) return null;
205
+ if (digits.length > 15 || scale >= POW10F.length) return null;
206
+ const int = Number(digits);
207
+ return {
208
+ int: neg$1 ? -int : int,
209
+ scale
210
+ };
211
+ };
212
+ /** Formats `int / 10^scale` as a canonical string, output identical to {@link format} (trailing zeros stripped, zero returns `'0'`). */
213
+ const fmtIntFast = (int, scale) => {
214
+ if (int === 0) return "0";
215
+ const neg$1 = int < 0;
216
+ let s = (neg$1 ? -int : int).toString();
217
+ if (scale === 0) return neg$1 ? `-${s}` : s;
218
+ if (s.length <= scale) s = "0".repeat(scale - s.length + 1) + s;
219
+ const cut = s.length - scale;
220
+ let end = s.length;
221
+ while (end > cut && s.charCodeAt(end - 1) === 48) end--;
222
+ const out = end === cut ? s.slice(0, cut) : `${s.slice(0, cut)}.${s.slice(cut, end)}`;
223
+ return neg$1 ? `-${out}` : out;
224
+ };
225
+ /** Fast path for addition/subtraction: `op` is `1` (add) or `-1` (subtract); returns `null` when unavailable. */
226
+ const fastAddSub = (a, b, op) => {
227
+ const pa = parseFast(a);
228
+ if (pa === null) return null;
229
+ const pb = parseFast(b);
230
+ if (pb === null) return null;
231
+ const scale = pa.scale > pb.scale ? pa.scale : pb.scale;
232
+ const ia = pa.int * POW10F[scale - pa.scale];
233
+ const ib = pb.int * POW10F[scale - pb.scale];
234
+ if (ia > MAX_SAFE || ia < -MAX_SAFE || ib > MAX_SAFE || ib < -MAX_SAFE) return null;
235
+ const r = ia + op * ib;
236
+ if (r > MAX_SAFE || r < -MAX_SAFE) return null;
237
+ return fmtIntFast(r, scale);
238
+ };
239
+ /** Fast path for multiplication; returns `null` when unavailable. */
240
+ const fastMul = (a, b) => {
241
+ const pa = parseFast(a);
242
+ if (pa === null) return null;
243
+ const pb = parseFast(b);
244
+ if (pb === null) return null;
245
+ const r = pa.int * pb.int;
246
+ if (r > MAX_SAFE || r < -MAX_SAFE) return null;
247
+ return fmtIntFast(r, pa.scale + pb.scale);
248
+ };
249
+ /**
250
+ * High-precision addition `a + b`, computed entirely with BigInt — no floating-point errors.
251
+ *
252
+ * @param a Augend
253
+ * @param b Addend
254
+ * @returns Canonical string of the sum
255
+ * @example
256
+ * add('0.1', '0.2') // '0.3'
257
+ */
258
+ const add$1 = (a, b) => {
259
+ const fast = fastAddSub(a, b, 1);
260
+ if (fast !== null) return fast;
261
+ const da = parse(a);
262
+ const db = parse(b);
263
+ const { aN, bN, exp } = align(da, db);
264
+ return format(fromBig(BigInt(da.sign) * aN + BigInt(db.sign) * bN, exp));
265
+ };
266
+ /**
267
+ * High-precision subtraction `a - b`.
268
+ *
269
+ * @param a Minuend
270
+ * @param b Subtrahend
271
+ * @returns Canonical string of the difference
272
+ * @example
273
+ * sub('0.3', '0.1') // '0.2'
274
+ */
275
+ const sub$1 = (a, b) => {
276
+ const fast = fastAddSub(a, b, -1);
277
+ if (fast !== null) return fast;
278
+ const da = parse(a);
279
+ const db = parse(b);
280
+ const { aN, bN, exp } = align(da, db);
281
+ return format(fromBig(BigInt(da.sign) * aN - BigInt(db.sign) * bN, exp));
282
+ };
283
+ /**
284
+ * High-precision multiplication `a * b`.
285
+ *
286
+ * @param a Factor
287
+ * @param b Factor
288
+ * @returns Canonical string of the product
289
+ * @example
290
+ * mul('0.1', '0.2') // '0.02'
291
+ */
292
+ const mul$1 = (a, b) => {
293
+ const fast = fastMul(a, b);
294
+ if (fast !== null) return fast;
295
+ const da = parse(a);
296
+ const db = parse(b);
297
+ const sign = da.sign * db.sign;
298
+ const digits = da.digits * db.digits;
299
+ const exp = da.exp + db.exp;
300
+ if (digits === 0n) return "0";
301
+ return format({
302
+ sign,
303
+ digits,
304
+ exp
305
+ });
306
+ };
307
+ /**
308
+ * High-precision division `a / b`, result rounded to `precision` decimal places (half-up).
309
+ *
310
+ * @param a Dividend
311
+ * @param b Divisor
312
+ * @param precision Maximum decimal places to retain, defaults to `50`
313
+ * @returns Canonical string of the quotient
314
+ * @throws When the divisor is zero
315
+ * @example
316
+ * div('1', '3') // '0.333...' (up to 50 places)
317
+ * div('1', '3', 4) // '0.3333'
318
+ */
319
+ const div$1 = (a, b, precision = 50) => {
320
+ const da = parse(a);
321
+ const db = parse(b);
322
+ if (db.digits === 0n) throw new Error("Division by zero");
323
+ if (da.digits === 0n) return "0";
324
+ const sign = da.sign * db.sign;
325
+ if (da.digits % db.digits === 0n) {
326
+ const q$1 = da.digits / db.digits;
327
+ const e = da.exp - db.exp;
328
+ return format(e >= 0 ? {
329
+ sign,
330
+ digits: q$1,
331
+ exp: e
332
+ } : {
333
+ sign,
334
+ digits: q$1 * pow10(-e),
335
+ exp: 0
336
+ });
337
+ }
338
+ const shift = precision + 1 + db.exp - da.exp;
339
+ let num = da.digits;
340
+ let den = db.digits;
341
+ if (shift >= 0) num = num * pow10(shift);
342
+ else den = den * pow10(-shift);
343
+ const q = num / den;
344
+ const rounded = q % TEN >= 5n ? q / TEN + 1n : q / TEN;
345
+ return format(fromBig(sign === 1 ? rounded : -rounded, precision));
346
+ };
347
+ /**
348
+ * Compares two numbers numerically.
349
+ *
350
+ * @param a Left operand
351
+ * @param b Right operand
352
+ * @returns `1` if `a > b`, `-1` if `a < b`, `0` if equal
353
+ * @example
354
+ * cmp('0.1', '0.2') // -1
355
+ * cmp('2', '2.0') // 0
356
+ */
357
+ const cmp = (a, b) => {
358
+ const da = parse(a);
359
+ const db = parse(b);
360
+ if (da.digits === 0n && db.digits === 0n) return 0;
361
+ if (da.sign !== db.sign) return da.sign < db.sign ? -1 : 1;
362
+ const { aN, bN } = align(da, db);
363
+ if (aN === bN) return 0;
364
+ return da.sign === 1 ? aN > bN ? 1 : -1 : aN > bN ? -1 : 1;
365
+ };
366
+ /**
367
+ * Returns the negation `-a`.
368
+ *
369
+ * @param a Input value
370
+ * @returns Canonical string of the negation (`0` always returns `'0'`)
371
+ * @example
372
+ * neg('1.5') // '-1.5'
373
+ * neg('-2') // '2'
374
+ */
375
+ const neg = (a) => {
376
+ const d = parse(a);
377
+ if (d.digits === 0n) return "0";
378
+ return format({
379
+ ...d,
380
+ sign: -d.sign
381
+ });
382
+ };
383
+ /**
384
+ * Returns the absolute value `|a|`.
385
+ *
386
+ * @param a Input value
387
+ * @returns Canonical string of the absolute value
388
+ * @example
389
+ * abs('-3.14') // '3.14'
390
+ */
391
+ const abs = (a) => {
392
+ return format({
393
+ ...parse(a),
394
+ sign: 1
395
+ });
396
+ };
397
+ /**
398
+ * Truncates to N decimal places by discarding excess digits — **no rounding**.
399
+ *
400
+ * @param a Input value
401
+ * @param decimals Number of decimal places to keep
402
+ * @returns Canonical string after truncation
403
+ * @example
404
+ * truncate('1.2349', 2) // '1.23'
405
+ * truncate('1.99', 0) // '1'
406
+ */
407
+ const truncate = (a, decimals) => {
408
+ const d = parse(a);
409
+ if (d.exp <= decimals) return format(d);
410
+ const drop = d.exp - decimals;
411
+ const newDigits = d.digits / TEN ** BigInt(drop);
412
+ if (newDigits === 0n) return "0";
413
+ return format({
414
+ sign: d.sign,
415
+ digits: newDigits,
416
+ exp: decimals
417
+ });
418
+ };
419
+ /**
420
+ * Rounds to N decimal places using half-up rounding (rounds up when digit `>= 0.5`).
421
+ *
422
+ * @param a Input value
423
+ * @param decimals Number of decimal places to keep
424
+ * @returns Canonical string after rounding
425
+ * @example
426
+ * roundHalfUp('1.235', 2) // '1.24'
427
+ * roundHalfUp('1.234', 2) // '1.23'
428
+ */
429
+ const roundHalfUp = (a, decimals) => {
430
+ const d = parse(a);
431
+ if (d.exp <= decimals) return format(d);
432
+ const drop = d.exp - decimals;
433
+ const divisor = TEN ** BigInt(drop);
434
+ const halved = TEN ** BigInt(drop - 1) * 5n;
435
+ const remainder = d.digits % divisor;
436
+ let q = d.digits / divisor;
437
+ if (remainder >= halved) q += 1n;
438
+ if (q === 0n) return "0";
439
+ return format({
440
+ sign: d.sign,
441
+ digits: q,
442
+ exp: decimals
443
+ });
444
+ };
445
+ /**
446
+ * Rounds away from zero to N decimal places (rounds up whenever there is any remainder).
447
+ *
448
+ * @param a Input value
449
+ * @param decimals Number of decimal places to keep
450
+ * @returns Canonical string after rounding
451
+ * @example
452
+ * roundCeil('1.231', 2) // '1.24'
453
+ * roundCeil('-1.231', 2) // '-1.24'
454
+ */
455
+ const roundCeil = (a, decimals) => {
456
+ const d = parse(a);
457
+ if (d.exp <= decimals) return format(d);
458
+ const drop = d.exp - decimals;
459
+ const divisor = TEN ** BigInt(drop);
460
+ const remainder = d.digits % divisor;
461
+ let q = d.digits / divisor;
462
+ if (remainder !== 0n) q += 1n;
463
+ if (q === 0n) return "0";
464
+ return format({
465
+ sign: d.sign,
466
+ digits: q,
467
+ exp: decimals
468
+ });
469
+ };
470
+ /**
471
+ * Banker's rounding (round half to even): when the discarded portion is exactly `0.5`,
472
+ * rounds to the nearest even digit.
473
+ *
474
+ * @param a Input value
475
+ * @param decimals Number of decimal places to keep
476
+ * @returns Canonical string after rounding
477
+ * @example
478
+ * roundBanker('0.5', 0) // '0'
479
+ * roundBanker('1.5', 0) // '2'
480
+ * roundBanker('2.5', 0) // '2'
481
+ */
482
+ const roundBanker = (a, decimals) => {
483
+ const d = parse(a);
484
+ if (d.exp <= decimals) return format(d);
485
+ const drop = d.exp - decimals;
486
+ const divisor = TEN ** BigInt(drop);
487
+ const halved = TEN ** BigInt(drop - 1) * 5n;
488
+ const remainder = d.digits % divisor;
489
+ let q = d.digits / divisor;
490
+ if (remainder > halved) q += 1n;
491
+ else if (remainder === halved && q % 2n === 1n) q += 1n;
492
+ if (q === 0n) return "0";
493
+ return format({
494
+ sign: d.sign,
495
+ digits: q,
496
+ exp: decimals
497
+ });
498
+ };
499
+
500
+ //#endregion
501
+ //#region src/utils/aggregate.ts
502
+ const pickValues = (keyOrArr, list) => {
503
+ let raw;
504
+ if (Array.isArray(keyOrArr)) raw = keyOrArr;
505
+ else {
506
+ if (!list) throw new Error("list is required when keyOrArr is a field name");
507
+ raw = list.map((item) => item[keyOrArr]);
508
+ }
509
+ return raw.filter((v) => v != null).map((v) => String(v));
510
+ };
511
+ /** Internal sum core — invalid values throw and propagate to the caller. */
512
+ const sumOf = (values) => {
513
+ if (values.length === 0) return "0";
514
+ let sum = values[0];
515
+ for (let i = 1; i < values.length; i++) sum = add$1(sum, values[i]);
516
+ return sum;
517
+ };
518
+ /**
519
+ * Computes the sum. Accepts two call forms: a direct value array, or a field name with an array of objects.
520
+ *
521
+ * @param keyOrArr Value array (`[1, 2, 3]`) or the field name to sum (`'price'`)
522
+ * @param list Array of objects, required when the first argument is a field name
523
+ * @returns Total sum (`string`, high precision)
524
+ * @example
525
+ * calcSum([1, 2, 3]) // '6'
526
+ * calcSum('price', [{ price: 10 }, { price: 20 }]) // '30'
527
+ */
528
+ const calcSum = (keyOrArr, list) => sumOf(pickValues(keyOrArr, list));
529
+ /** Computes the average with the given config precision (shared by the default {@link calcAvg} export and per-call precision entry points). */
530
+ const calcAvgWith = (cfg, keyOrArr, list) => {
531
+ const values = pickValues(keyOrArr, list);
532
+ if (values.length === 0) return "0";
533
+ return div$1(sumOf(values), String(values.length), cfg._precision);
534
+ };
535
+ function calcAvg(keyOrArr, listOrOpt, opt) {
536
+ const isFieldForm = Array.isArray(listOrOpt);
537
+ const list = isFieldForm ? listOrOpt : void 0;
538
+ return calcAvgWith(configWithPrecision(isFieldForm ? opt : listOrOpt), keyOrArr, list);
539
+ }
540
+ /**
541
+ * Returns the maximum value (numeric comparison, not lexicographic).
542
+ *
543
+ * @param keyOrArr Value array or field name
544
+ * @param list Array of objects, required when the first argument is a field name
545
+ * @returns Maximum value (`string`); returns `'0'` for an empty collection
546
+ * @example
547
+ * calcMax([3, 10, 2]) // '10'
548
+ */
549
+ const calcMax = (keyOrArr, list) => {
550
+ const values = pickValues(keyOrArr, list);
551
+ if (values.length === 0) return "0";
552
+ let max = values[0];
553
+ for (let i = 1; i < values.length; i++) if (cmp(values[i], max) > 0) max = values[i];
554
+ return max;
555
+ };
556
+ /**
557
+ * Returns the minimum value (numeric comparison).
558
+ *
559
+ * @param keyOrArr Value array or field name
560
+ * @param list Array of objects, required when the first argument is a field name
561
+ * @returns Minimum value (`string`); returns `'0'` for an empty collection
562
+ * @example
563
+ * calcMin([3, 10, 2]) // '2'
564
+ */
565
+ const calcMin = (keyOrArr, list) => {
566
+ const values = pickValues(keyOrArr, list);
567
+ if (values.length === 0) return "0";
568
+ let min = values[0];
569
+ for (let i = 1; i < values.length; i++) if (cmp(values[i], min) < 0) min = values[i];
570
+ return min;
571
+ };
572
+
573
+ //#endregion
574
+ //#region src/utils/parser.ts
575
+ const RE_WS = /\s/;
576
+ const RE_DIGIT = /\d/;
577
+ const RE_NUM_BODY = /[\d.]/;
578
+ const RE_IDENT_START = /[a-z_$\u4E00-\u9FA5]/i;
579
+ const RE_IDENT_BODY = /[\w$\u4E00-\u9FA5]/;
580
+ const SINGLE_OPS = {
581
+ "+": "PLUS",
582
+ "-": "MINUS",
583
+ "*": "STAR",
584
+ "/": "SLASH",
585
+ "(": "LPAREN",
586
+ ")": "RPAREN",
587
+ ",": "COMMA"
588
+ };
589
+ const tokenize = (input) => {
590
+ const tokens = [];
591
+ let i = 0;
592
+ while (i < input.length) {
593
+ const c = input[i];
594
+ if (RE_WS.test(c)) {
595
+ i++;
596
+ continue;
597
+ }
598
+ if (RE_DIGIT.test(c) || c === "." && RE_DIGIT.test(input[i + 1] || "")) {
599
+ const start = i;
600
+ while (i < input.length && RE_NUM_BODY.test(input[i])) i++;
601
+ if (input[i] === "e" || input[i] === "E") {
602
+ i++;
603
+ if (input[i] === "+" || input[i] === "-") i++;
604
+ while (i < input.length && RE_DIGIT.test(input[i])) i++;
605
+ }
606
+ tokens.push({
607
+ type: "NUMBER",
608
+ value: input.slice(start, i),
609
+ pos: start
610
+ });
611
+ if (input[i] === "%" && input[i + 1] !== "%") {
612
+ tokens.push({
613
+ type: "UNIT",
614
+ value: "%",
615
+ pos: i
616
+ });
617
+ i++;
618
+ }
619
+ continue;
620
+ }
621
+ if (RE_IDENT_START.test(c)) {
622
+ const start = i;
623
+ i++;
624
+ while (i < input.length && RE_IDENT_BODY.test(input[i])) i++;
625
+ tokens.push({
626
+ type: "IDENT",
627
+ value: input.slice(start, i),
628
+ pos: start
629
+ });
630
+ continue;
631
+ }
632
+ if (c in SINGLE_OPS) {
633
+ tokens.push({
634
+ type: SINGLE_OPS[c],
635
+ value: c,
636
+ pos: i
637
+ });
638
+ i++;
639
+ continue;
640
+ }
641
+ throw new Error(`Illegal character at position ${i}: "${c}"`);
642
+ }
643
+ tokens.push({
644
+ type: "EOF",
645
+ value: "",
646
+ pos: input.length
647
+ });
648
+ return tokens;
649
+ };
650
+ /** Floor (toward -∞, same as Math.floor) */
651
+ const mathFloor = (x) => {
652
+ const t = truncate(x, 0);
653
+ return cmp(x, "0") < 0 && cmp(x, t) !== 0 ? sub$1(t, "1") : t;
654
+ };
655
+ /** Ceiling (toward +∞, same as Math.ceil) */
656
+ const mathCeil = (x) => {
657
+ const t = truncate(x, 0);
658
+ return cmp(x, "0") > 0 && cmp(x, t) !== 0 ? add$1(t, "1") : t;
659
+ };
660
+ /** Sign: -1 / 0 / 1 */
661
+ const mathSign = (x) => {
662
+ const c = cmp(x, "0");
663
+ return c > 0 ? "1" : c < 0 ? "-1" : "0";
664
+ };
665
+ /** Integer exponentiation (exponent must be an integer; negative exponents use division) */
666
+ const mathPow = (base, expStr, precision) => {
667
+ const e = Number(expStr);
668
+ if (!Number.isInteger(e)) throw new Error(`pow() exponent must be an integer: "${expStr}"`);
669
+ let r = "1";
670
+ for (let i = 0; i < Math.abs(e); i++) r = mul$1(r, base);
671
+ return e < 0 ? div$1("1", r, precision) : r;
672
+ };
673
+ /** Modulo (remainder has the same sign as the dividend, same as JS %) */
674
+ const mathMod = (a, b, precision) => {
675
+ if (cmp(b, "0") === 0) throw new Error("mod division by zero");
676
+ return sub$1(a, mul$1(truncate(div$1(a, b, precision), 0), b));
677
+ };
678
+ /** Pick a value from args by comparison (used for min / max) */
679
+ const pickBy = (name, args, keep) => {
680
+ if (args.length === 0) throw new Error(`${name}() requires at least 1 argument`);
681
+ let r = args[0];
682
+ for (let i = 1; i < args.length; i++) if (keep(cmp(args[i], r))) r = args[i];
683
+ return r;
684
+ };
685
+ const FN_ARITY = {
686
+ abs: 1,
687
+ sign: 1,
688
+ floor: 1,
689
+ ceil: 1,
690
+ round: 1,
691
+ trunc: 1,
692
+ pow: 2,
693
+ mod: 2,
694
+ clamp: 3
695
+ };
696
+ /** Built-in functions (available inside expressions) */
697
+ const applyFn = (name, args, precision) => {
698
+ const arity = FN_ARITY[name];
699
+ if (arity !== void 0 && args.length !== arity) throw new Error(`${name}() expects ${arity} argument(s), got ${args.length}`);
700
+ switch (name) {
701
+ case "abs": return abs(args[0]);
702
+ case "sign": return mathSign(args[0]);
703
+ case "floor": return mathFloor(args[0]);
704
+ case "ceil": return mathCeil(args[0]);
705
+ case "round": return mathFloor(add$1(args[0], "0.5"));
706
+ case "trunc": return truncate(args[0], 0);
707
+ case "pow": return mathPow(args[0], args[1], precision);
708
+ case "mod": return mathMod(args[0], args[1], precision);
709
+ case "min": return pickBy("min", args, (c) => c < 0);
710
+ case "max": return pickBy("max", args, (c) => c > 0);
711
+ case "clamp": {
712
+ const [x, lo, hi] = args;
713
+ if (cmp(x, lo) < 0) return lo;
714
+ if (cmp(x, hi) > 0) return hi;
715
+ return x;
716
+ }
717
+ default: throw new Error(`Unknown function: "${name}()"`);
718
+ }
719
+ };
720
+ var Parser = class {
721
+ tokens;
722
+ pos = 0;
723
+ constructor(input, ctx) {
724
+ this.ctx = ctx;
725
+ this.tokens = tokenize(input);
726
+ }
727
+ peek() {
728
+ return this.tokens[this.pos];
729
+ }
730
+ consume() {
731
+ return this.tokens[this.pos++];
732
+ }
733
+ match(...types) {
734
+ if (types.includes(this.peek().type)) return this.consume();
735
+ return null;
736
+ }
737
+ expect(type) {
738
+ const t = this.peek();
739
+ if (t.type !== type) throw new Error(`Expected ${type} at position ${t.pos}, got ${t.type}("${t.value}")`);
740
+ return this.consume();
741
+ }
742
+ step(line) {
743
+ if (this.ctx.trace) this.ctx.trace.push(line);
744
+ }
745
+ parse() {
746
+ const v = this.addSub();
747
+ if (this.peek().type !== "EOF") {
748
+ const t = this.peek();
749
+ throw new Error(`Unexpected token "${t.value}" at position ${t.pos} — expression not fully parsed`);
750
+ }
751
+ return v;
752
+ }
753
+ addSub() {
754
+ let left = this.mulDiv();
755
+ while (true) {
756
+ const op = this.match("PLUS", "MINUS");
757
+ if (!op) break;
758
+ const right = this.mulDiv();
759
+ const prev = left;
760
+ left = op.type === "PLUS" ? add$1(left, right) : sub$1(left, right);
761
+ this.step(`${prev} ${op.value} ${right} = ${left}`);
762
+ }
763
+ return left;
764
+ }
765
+ mulDiv() {
766
+ let left = this.unary();
767
+ while (true) {
768
+ const op = this.match("STAR", "SLASH");
769
+ if (!op) break;
770
+ const right = this.unary();
771
+ const prev = left;
772
+ left = op.type === "STAR" ? mul$1(left, right) : div$1(left, right, this.ctx.precision);
773
+ this.step(`${prev} ${op.value} ${right} = ${left}`);
774
+ }
775
+ return left;
776
+ }
777
+ unary() {
778
+ if (this.match("PLUS")) return this.unary();
779
+ if (this.match("MINUS")) return neg(this.unary());
780
+ return this.primary();
781
+ }
782
+ primary() {
783
+ const t = this.peek();
784
+ if (t.type === "NUMBER") {
785
+ this.consume();
786
+ const val = t.value;
787
+ if (this.peek().type === "UNIT") {
788
+ if (this.consume().value === "%") {
789
+ if (this.ctx.unit) return val;
790
+ return div$1(val, "100", this.ctx.precision);
791
+ }
792
+ }
793
+ return val;
794
+ }
795
+ if (t.type === "IDENT") {
796
+ this.consume();
797
+ if (this.peek().type === "LPAREN") return this.callFn(t.value);
798
+ throw new Error(`Unknown identifier: "${t.value}" (calc supports arithmetic and math functions only; use template interpolation for values)`);
799
+ }
800
+ if (t.type === "LPAREN") {
801
+ this.consume();
802
+ const v = this.addSub();
803
+ this.expect("RPAREN");
804
+ return v;
805
+ }
806
+ throw new Error(`Unexpected token at position ${t.pos}: ${t.type}("${t.value}")`);
807
+ }
808
+ callFn(name) {
809
+ this.expect("LPAREN");
810
+ const args = [];
811
+ if (this.peek().type !== "RPAREN") {
812
+ args.push(this.addSub());
813
+ while (this.match("COMMA")) args.push(this.addSub());
814
+ }
815
+ this.expect("RPAREN");
816
+ const r = applyFn(name, args, this.ctx.precision);
817
+ this.step(`${name}(${args.join(", ")}) = ${r}`);
818
+ return r;
819
+ }
820
+ };
821
+ /**
822
+ * Parse and evaluate an expression (the pure arithmetic part, without any format pipeline).
823
+ *
824
+ * Most callers should use {@link calc}; this is the low-level evaluator for cases where
825
+ * direct control over the evaluation context is needed.
826
+ *
827
+ * @param expr Pure expression string (no format pipe)
828
+ * @param ctx Evaluation context: unit mode flag and division precision
829
+ * @returns Canonical string representation of the evaluated result
830
+ * @throws Throws on lexical or syntax errors
831
+ * @example
832
+ * evaluate('1 + 2', { unit: false, precision: 50 }) // '3'
833
+ */
834
+ const evaluate = (expr, ctx) => {
835
+ return new Parser(expr, ctx).parse();
836
+ };
837
+
838
+ //#endregion
839
+ //#region src/utils/format.ts
840
+ const ROUNDING_ALIAS = {
841
+ truncate: "truncate",
842
+ trunc: "truncate",
843
+ halfUp: "halfUp",
844
+ round: "halfUp",
845
+ banker: "banker",
846
+ halfEven: "banker",
847
+ ceil: "ceil"
848
+ };
849
+ const OUTPUT_FLAG = {
850
+ "percent": "percent",
851
+ "%%": "percent",
852
+ "fraction": "fraction",
853
+ "//": "fraction",
854
+ "scientific": "scientific",
855
+ "e": "scientific",
856
+ "number": "asNumber",
857
+ "num": "asNumber"
858
+ };
859
+ const RE_THOUSANDS_GROUPS = /\B(?=(?:\d{3})+(?!\d))/g;
860
+ const RE_TRAIL_ZERO = /0+$/;
861
+ const RE_DOT_END = /\.$/;
862
+ const RE_ZERO_VAL = /^0+(?:\.0+)?$/;
863
+ const RE_COMMA_GLOBAL = /,/g;
864
+ const applyRounding = (value, decimals, mode) => {
865
+ switch (mode) {
866
+ case "halfUp": return roundHalfUp(value, decimals);
867
+ case "banker": return roundBanker(value, decimals);
868
+ case "ceil": return roundCeil(value, decimals);
869
+ case "truncate":
870
+ default: return truncate(value, decimals);
871
+ }
872
+ };
873
+ const applyThousands = (intStr, preset) => {
874
+ if (preset === "in") {
875
+ if (intStr.length <= 3) return intStr;
876
+ const last3 = intStr.slice(-3);
877
+ const restPart = intStr.slice(0, -3);
878
+ const parts = [];
879
+ let r = restPart;
880
+ while (r.length > 2) {
881
+ parts.unshift(r.slice(-2));
882
+ r = r.slice(0, -2);
883
+ }
884
+ if (r) parts.unshift(r);
885
+ return `${parts.join(",")},${last3}`;
886
+ }
887
+ return intStr.replace(RE_THOUSANDS_GROUPS, ",");
888
+ };
889
+ const COMPACT_PRESETS = {
890
+ default: [
891
+ {
892
+ scale: 12,
893
+ suffix: "T"
894
+ },
895
+ {
896
+ scale: 9,
897
+ suffix: "B"
898
+ },
899
+ {
900
+ scale: 6,
901
+ suffix: "M"
902
+ },
903
+ {
904
+ scale: 3,
905
+ suffix: "K"
906
+ }
907
+ ],
908
+ zh: [
909
+ {
910
+ scale: 12,
911
+ suffix: "万亿"
912
+ },
913
+ {
914
+ scale: 8,
915
+ suffix: "亿"
916
+ },
917
+ {
918
+ scale: 4,
919
+ suffix: "万"
920
+ }
921
+ ]
922
+ };
923
+ const applyCompact = (value, preset) => {
924
+ const presets = COMPACT_PRESETS[preset || "default"] || COMPACT_PRESETS.default;
925
+ const absValue = abs(value);
926
+ for (const p of presets) {
927
+ const threshold = `1${"0".repeat(p.scale)}`;
928
+ if (cmp(absValue, threshold) >= 0) return {
929
+ num: div$1(value, threshold, 20),
930
+ suffix: p.suffix
931
+ };
932
+ }
933
+ return {
934
+ num: value,
935
+ suffix: ""
936
+ };
937
+ };
938
+ const applyScientific = (value) => {
939
+ const d = parse(value);
940
+ if (d.digits === 0n) return "0e+0";
941
+ const digitStr = d.digits.toString();
942
+ const e = digitStr.length - 1 - d.exp;
943
+ const mantissa = digitStr.length === 1 ? digitStr : `${digitStr[0]}.${digitStr.slice(1).replace(RE_TRAIL_ZERO, "")}`.replace(RE_DOT_END, "");
944
+ return `${d.sign < 0 ? "-" : ""}${mantissa}e${e >= 0 ? "+" : ""}${e}`;
945
+ };
946
+ const gcd = (a, b) => b === 0n ? a : gcd(b, a % b);
947
+ const applyFraction = (value) => {
948
+ const d = parse(value);
949
+ if (d.digits === 0n) return "0";
950
+ if (d.exp === 0) return d.sign < 0 ? `-${d.digits}/1` : `${d.digits}/1`;
951
+ let num = d.digits;
952
+ let den = 10n ** BigInt(d.exp);
953
+ const g = gcd(num, den);
954
+ num /= g;
955
+ den /= g;
956
+ return `${d.sign < 0 ? "-" : ""}${num}/${den}`;
957
+ };
958
+ const padDecimals = (v, n) => {
959
+ const dot = v.indexOf(".");
960
+ if (n === 0) return dot === -1 ? v : v.slice(0, dot);
961
+ if (dot === -1) return `${v}.${"0".repeat(n)}`;
962
+ const fracLen = v.length - dot - 1;
963
+ if (fracLen >= n) return v;
964
+ return v + "0".repeat(n - fracLen);
965
+ };
966
+ const padIntegerZeros = (v, n) => {
967
+ let sign = "";
968
+ let s = v;
969
+ if (s.startsWith("-")) {
970
+ sign = "-";
971
+ s = s.slice(1);
972
+ } else if (s.startsWith("+")) {
973
+ sign = "+";
974
+ s = s.slice(1);
975
+ }
976
+ const dot = s.indexOf(".");
977
+ const intPart = dot === -1 ? s : s.slice(0, dot);
978
+ const rest = dot === -1 ? "" : s.slice(dot);
979
+ if (intPart.length >= n) return sign + intPart + rest;
980
+ return sign + intPart.padStart(n, "0") + rest;
981
+ };
982
+ const applyThousandsAndPreset = (v, preset) => {
983
+ let sign = "";
984
+ let s = v;
985
+ if (s.startsWith("-")) {
986
+ sign = "-";
987
+ s = s.slice(1);
988
+ } else if (s.startsWith("+")) {
989
+ sign = "+";
990
+ s = s.slice(1);
991
+ }
992
+ const dot = s.indexOf(".");
993
+ const intPart = dot === -1 ? s : s.slice(0, dot);
994
+ const fracPart = dot === -1 ? "" : s.slice(dot + 1);
995
+ const grouped = applyThousands(intPart, preset);
996
+ if (preset === "eu") {
997
+ const groupedEu = grouped.replace(RE_COMMA_GLOBAL, ".");
998
+ return sign + groupedEu + (fracPart ? `,${fracPart}` : "");
999
+ }
1000
+ return sign + grouped + (fracPart ? `.${fracPart}` : "");
1001
+ };
1002
+ const finalDecorate = (v, opts, compactSuffix) => {
1003
+ let s = v;
1004
+ if (compactSuffix) s += compactSuffix;
1005
+ if (opts.percent) s += "%";
1006
+ if (opts.asNumber) return Number(s);
1007
+ return s;
1008
+ };
1009
+ /**
1010
+ * Formats a numeric string according to {@link IFormatOpts} (rounding, zero-padding,
1011
+ * thousands separator, compact notation, percent, etc.).
1012
+ *
1013
+ * Most callers should prefer {@link fmt}; this function accepts already-normalized flat options.
1014
+ *
1015
+ * @param value Canonical numeric string (e.g. the result of a precision arithmetic operation)
1016
+ * @param opts Formatting options produced by {@link normalizeFormat}
1017
+ * @returns Formatted result — `number` when `output: 'number'`, `string` otherwise
1018
+ */
1019
+ const formatValue = (value, opts) => {
1020
+ let v = value;
1021
+ if (opts.clampMin !== void 0 && cmp(v, opts.clampMin) < 0) v = opts.clampMin;
1022
+ if (opts.clampMax !== void 0 && cmp(v, opts.clampMax) > 0) v = opts.clampMax;
1023
+ if (opts.percent) v = mul$1(v, "100");
1024
+ let compactSuffix = "";
1025
+ if (opts.compact) {
1026
+ const c = applyCompact(v, opts.compactPreset);
1027
+ v = c.num;
1028
+ compactSuffix = c.suffix;
1029
+ }
1030
+ if (opts.fraction) return finalDecorate(applyFraction(v), opts, compactSuffix);
1031
+ if (opts.scientific) return finalDecorate(applyScientific(v), opts, compactSuffix);
1032
+ const rounding = opts.rounding || "truncate";
1033
+ if (opts.fixed !== void 0) {
1034
+ v = applyRounding(v, opts.fixed, rounding);
1035
+ v = padDecimals(v, opts.fixed);
1036
+ } else {
1037
+ if (opts.max !== void 0) v = applyRounding(v, opts.max, rounding);
1038
+ if (opts.min !== void 0) v = padDecimals(v, opts.min);
1039
+ }
1040
+ if (opts.intPad !== void 0) v = padIntegerZeros(v, opts.intPad);
1041
+ if (opts.thousands) v = applyThousandsAndPreset(v, opts.thousandsPreset);
1042
+ if (opts.plus && !v.startsWith("-") && !RE_ZERO_VAL.test(v)) v = `+${v}`;
1043
+ return finalDecorate(v, opts, compactSuffix);
1044
+ };
1045
+ /**
1046
+ * Normalizes a high-level {@link IFormat} object into the internal flat options structure.
1047
+ *
1048
+ * @param format An {@link IFormat} object
1049
+ * @returns Flat {@link IFormatOpts}
1050
+ */
1051
+ const normalizeFormat = (format$1) => {
1052
+ if (!format$1) return {};
1053
+ const o = {};
1054
+ const f = format$1;
1055
+ if (typeof f.decimals === "number") o.fixed = f.decimals;
1056
+ else if (f.decimals) {
1057
+ if (f.decimals.max !== void 0) o.max = f.decimals.max;
1058
+ if (f.decimals.min !== void 0) o.min = f.decimals.min;
1059
+ }
1060
+ if (f.rounding) o.rounding = ROUNDING_ALIAS[f.rounding];
1061
+ if (f.thousands) {
1062
+ o.thousands = true;
1063
+ if (f.thousands !== true) o.thousandsPreset = f.thousands;
1064
+ }
1065
+ if (f.compact) {
1066
+ o.compact = true;
1067
+ if (f.compact !== true) o.compactPreset = f.compact;
1068
+ }
1069
+ if (f.clamp) {
1070
+ o.clampMin = String(f.clamp[0]);
1071
+ o.clampMax = String(f.clamp[1]);
1072
+ }
1073
+ const outFlag = f.output && OUTPUT_FLAG[f.output];
1074
+ if (outFlag) o[outFlag] = true;
1075
+ if (f.plus) o.plus = true;
1076
+ if (f.pad !== void 0) o.intPad = f.pad;
1077
+ return o;
1078
+ };
1079
+ const RE_PERCENT = /%/;
1080
+ /**
1081
+ * Display-oriented formatting: the display counterpart of {@link calc} — same API,
1082
+ * **supports arithmetic** (the first argument may be an arithmetic expression string),
1083
+ * but **returns `_error` as a fallback on failure instead of throwing**, making it safe
1084
+ * for direct use in template rendering (e.g. `{{ fmt(`${price} * ${qty}`, { decimals: 2 }) }}`).
1085
+ *
1086
+ * - Pass a `string` ⇒ evaluate as an arithmetic expression, then format (`fmt('1 + 2 * 3')` ⇒ `'7'`)
1087
+ * - Pass a `number` / `bigint` ⇒ format directly
1088
+ * - On error (invalid expression, etc.) ⇒ return the `_error` fallback
1089
+ * (local `options._error` takes precedence over the global default `'-'`)
1090
+ *
1091
+ * @returns Formatted result; the `_error` fallback value on failure
1092
+ * @example
1093
+ * fmt(1234.5, { decimals: 2, thousands: true }) // '1,234.50'
1094
+ * fmt('999.99 * 3', { decimals: 2 }) // '2,999.97' (evaluate first, then format)
1095
+ * fmt('bad expr') // '-' (fallback, does not throw)
1096
+ */
1097
+ function fmtWith(cfg, value, options) {
1098
+ try {
1099
+ const v = typeof value === "string" ? evaluate(value, {
1100
+ unit: !!options?._unit,
1101
+ precision: options?._precision ?? cfg._precision,
1102
+ trace: void 0
1103
+ }) : format(parse(value));
1104
+ const opts = normalizeFormat(options);
1105
+ const out = formatValue(v, opts);
1106
+ if (typeof value === "string" && options?._unit && RE_PERCENT.test(value) && typeof out === "string" && !opts.percent && !opts.fraction && !opts.scientific && !opts.asNumber) return `${out}%`;
1107
+ return out;
1108
+ } catch {
1109
+ return options?._error !== void 0 ? options._error : cfg._error;
1110
+ }
1111
+ }
1112
+ const fmt = (value, options) => fmtWith(getConfig(), value, options);
1113
+
1114
+ //#endregion
1115
+ //#region src/utils/calc.ts
1116
+ const RE_HAS_PERCENT = /%/;
1117
+ const emitDebug = (info, debug) => {
1118
+ if (typeof debug === "function") {
1119
+ debug(info);
1120
+ return;
1121
+ }
1122
+ console.log(`[calc] ${info.expr}\n${info.steps.length ? `${info.steps.map((s) => ` · ${s}`).join("\n")}\n` : ""} = ${info.result}`);
1123
+ };
1124
+ /** Evaluate with an explicit config (the default export {@link calc} delegates to this). */
1125
+ function calcWith(cfg, expr, options = {}) {
1126
+ const steps = options._debug ? [] : void 0;
1127
+ const value = evaluate(expr, {
1128
+ unit: !!options._unit,
1129
+ precision: options._precision ?? cfg._precision,
1130
+ trace: steps
1131
+ });
1132
+ const exprHasPercent = options._unit && RE_HAS_PERCENT.test(expr);
1133
+ const fmtSpec = options._fmt || cfg._fmt;
1134
+ const result = !fmtSpec ? exprHasPercent ? `${value}%` : value : formatValue(value, normalizeFormat(fmtSpec));
1135
+ if (options._debug) emitDebug({
1136
+ expr,
1137
+ steps,
1138
+ value,
1139
+ result
1140
+ }, options._debug);
1141
+ return result;
1142
+ }
1143
+ /**
1144
+ * Main entry point: evaluates an arithmetic expression string and optionally formats the output.
1145
+ *
1146
+ * Expressions are pure arithmetic: the four basic operations, parentheses, and math functions
1147
+ * (`max`/`min`/`clamp`…). **Variables are not supported** — embed values directly in the
1148
+ * expression via template interpolation: `` calc(`${price} * ${qty}`) ``.
1149
+ * Formatting is specified via `options._fmt` (an {@link IFormat} object).
1150
+ *
1151
+ * `calc` is designed for **computation**: on error (invalid expression, etc.) it **throws
1152
+ * directly** and the caller is responsible for handling it.
1153
+ * For **display** scenarios that need a fallback on error, use {@link fmt} instead
1154
+ * (same API and supports arithmetic, but returns `_error` on failure rather than throwing).
1155
+ *
1156
+ * @param expr Arithmetic expression, e.g. `'1 + 2 * 3'`, `'(1 + 2) / 3'`
1157
+ * @param options Control options (see {@link ICalcOptions})
1158
+ * @returns Computation result (`string`; `number` when `_fmt.output` is `'number'`)
1159
+ * @throws Throws when the expression is invalid
1160
+ * @example
1161
+ * calc('1 + 2 * 3') // '7'
1162
+ * calc(`${price} * ${qty}`) // use template interpolation instead of variables
1163
+ * calc('1 + 2', { _fmt: { decimals: 2 } }) // '3.00'
1164
+ */
1165
+ const calc = (expr, options = {}) => calcWith(getConfig(), expr, options);
1166
+
1167
+ //#endregion
1168
+ //#region src/utils/chain.ts
1169
+ /** Extracts a per-call precision option from the end of a variadic argument list (last element is an object ⇒ treated as {@link IPrecisionOption}). */
1170
+ const splitPrecision$1 = (args) => {
1171
+ const last = args.at(-1);
1172
+ if (last != null && typeof last === "object") return [args.slice(0, -1), last];
1173
+ return [args, void 0];
1174
+ };
1175
+ const makeChain = (cfg, initial) => {
1176
+ let value = initial;
1177
+ const fn = ((format$1) => {
1178
+ if (!format$1) return value;
1179
+ return formatValue(value, normalizeFormat(format$1));
1180
+ });
1181
+ fn.add = (...args) => {
1182
+ for (const a of args) value = add$1(value, a);
1183
+ return fn;
1184
+ };
1185
+ fn.sub = (...args) => {
1186
+ for (const a of args) value = sub$1(value, a);
1187
+ return fn;
1188
+ };
1189
+ fn.mul = (...args) => {
1190
+ for (const a of args) value = mul$1(value, a);
1191
+ return fn;
1192
+ };
1193
+ fn.div = (...args) => {
1194
+ for (const a of args) value = div$1(value, a, cfg._precision);
1195
+ return fn;
1196
+ };
1197
+ return fn;
1198
+ };
1199
+ const reduceWith = (op, args) => {
1200
+ if (args.length === 0) return "0";
1201
+ let r = String(args[0]);
1202
+ for (let i = 1; i < args.length; i++) r = op(r, args[i]);
1203
+ return r;
1204
+ };
1205
+ const chainAddWith = (cfg, ...args) => makeChain(cfg, reduceWith(add$1, args));
1206
+ const chainSubWith = (cfg, ...args) => makeChain(cfg, reduceWith(sub$1, args));
1207
+ const chainMulWith = (cfg, ...args) => makeChain(cfg, reduceWith(mul$1, args));
1208
+ const chainDivWith = (cfg, ...args) => makeChain(cfg, reduceWith((a, b) => div$1(a, b, cfg._precision), args));
1209
+ /**
1210
+ * Starts a chaining computation with addition (`a + b + c ...`).
1211
+ *
1212
+ * @param args Initial values to accumulate via addition
1213
+ * @returns An {@link IChain} supporting further `.add().sub().mul().div()` calls
1214
+ * @example
1215
+ * chainAdd(1, 2).mul(3)() // '9'
1216
+ * chainAdd(1, 2).mul(3)({ decimals: 2 }) // '9.00'
1217
+ */
1218
+ const chainAdd = (...args) => chainAddWith(getConfig(), ...args);
1219
+ /**
1220
+ * Starts a chaining computation with subtraction (`a - b - c ...`).
1221
+ *
1222
+ * @param args Initial minuend and subtrahends
1223
+ * @returns An {@link IChain} supporting further chaining
1224
+ * @example
1225
+ * chainSub(10, 1, 2)() // '7'
1226
+ */
1227
+ const chainSub = (...args) => chainSubWith(getConfig(), ...args);
1228
+ /**
1229
+ * Starts a chaining computation with multiplication (`a * b * c ...`).
1230
+ *
1231
+ * @param args Initial factors to multiply together
1232
+ * @returns An {@link IChain} supporting further chaining
1233
+ * @example
1234
+ * chainMul(2, 3).add(4)() // '10'
1235
+ */
1236
+ const chainMul = (...args) => chainMulWith(getConfig(), ...args);
1237
+ function chainDiv(...args) {
1238
+ const [values, opt] = splitPrecision$1(args);
1239
+ return chainDivWith(configWithPrecision(opt), ...values);
1240
+ }
1241
+
1242
+ //#endregion
1243
+ //#region src/utils/standalone.ts
1244
+ const reduce = (op, args) => {
1245
+ if (args.length === 0) return "0";
1246
+ let r = String(args[0]);
1247
+ for (let i = 1; i < args.length; i++) r = op(r, args[i]);
1248
+ return r;
1249
+ };
1250
+ /** Extracts a per-call precision option from the end of a variadic argument list (last element is an object ⇒ treated as {@link IPrecisionOption}; Val is always string/number/bigint so there is no ambiguity). */
1251
+ const splitPrecision = (args) => {
1252
+ const last = args.at(-1);
1253
+ if (last != null && typeof last === "object") return [args.slice(0, -1), last];
1254
+ return [args, void 0];
1255
+ };
1256
+ /** Division using the precision from the given config (shared by the default {@link div} export and per-call precision entry points). */
1257
+ const divWith = (cfg, ...args) => Number(reduce((a, b) => div$1(a, b, cfg._precision), args));
1258
+ /** Division using the given config precision, string-returning variant (shared implementation). */
1259
+ const divStrWith = (cfg, ...args) => reduce((a, b) => div$1(a, b, cfg._precision), args);
1260
+ /**
1261
+ * High-precision addition, result converted to `number` (convenient for interop with native numbers).
1262
+ *
1263
+ * @param args Any number of addends, accumulated left to right
1264
+ * @returns Sum (`number`)
1265
+ * @example
1266
+ * add(0.1, 0.2) // 0.3
1267
+ * add(1, 2, 3, 4) // 10
1268
+ */
1269
+ const add = (...args) => Number(reduce(add$1, args));
1270
+ /**
1271
+ * High-precision subtraction, result as `number`: `args[0] - args[1] - args[2] ...`.
1272
+ *
1273
+ * @param args Minuend followed by subtrahends
1274
+ * @returns Difference (`number`)
1275
+ * @example
1276
+ * sub(0.3, 0.1) // 0.2
1277
+ */
1278
+ const sub = (...args) => Number(reduce(sub$1, args));
1279
+ /**
1280
+ * High-precision multiplication, result as `number`, factors multiplied left to right.
1281
+ *
1282
+ * @param args Any number of factors
1283
+ * @returns Product (`number`)
1284
+ * @example
1285
+ * mul(0.1, 0.2) // 0.02
1286
+ */
1287
+ const mul = (...args) => Number(reduce(mul$1, args));
1288
+ function div(...args) {
1289
+ const [values, opt] = splitPrecision(args);
1290
+ return divWith(configWithPrecision(opt), ...values);
1291
+ }
1292
+ /**
1293
+ * High-precision addition, result returned as `string` (no precision loss; suitable for monetary values).
1294
+ *
1295
+ * @param args Any number of addends
1296
+ * @returns Sum (`string`)
1297
+ * @example
1298
+ * addStr('0.1', '0.2') // '0.3'
1299
+ */
1300
+ const addStr = (...args) => reduce(add$1, args);
1301
+ /**
1302
+ * High-precision subtraction, result as `string`: `args[0] - args[1] - ...`.
1303
+ *
1304
+ * @param args Minuend followed by subtrahends
1305
+ * @returns Difference (`string`)
1306
+ * @example
1307
+ * subStr('0.3', '0.1') // '0.2'
1308
+ */
1309
+ const subStr = (...args) => reduce(sub$1, args);
1310
+ /**
1311
+ * High-precision multiplication, result as `string`, factors multiplied left to right.
1312
+ *
1313
+ * @param args Any number of factors
1314
+ * @returns Product (`string`)
1315
+ * @example
1316
+ * mulStr('0.1', '0.2') // '0.02'
1317
+ */
1318
+ const mulStr = (...args) => reduce(mul$1, args);
1319
+ function divStr(...args) {
1320
+ const [values, opt] = splitPrecision(args);
1321
+ return divStrWith(configWithPrecision(opt), ...values);
1322
+ }
1323
+
1324
+ //#endregion
1325
+ exports.abs = abs;
1326
+ exports.add = add;
1327
+ exports.addStr = addStr;
1328
+ exports.calc = calc;
1329
+ exports.calcAvg = calcAvg;
1330
+ exports.calcMax = calcMax;
1331
+ exports.calcMin = calcMin;
1332
+ exports.calcSum = calcSum;
1333
+ exports.chainAdd = chainAdd;
1334
+ exports.chainDiv = chainDiv;
1335
+ exports.chainMul = chainMul;
1336
+ exports.chainSub = chainSub;
1337
+ exports.cmp = cmp;
1338
+ exports.div = div;
1339
+ exports.divStr = divStr;
1340
+ exports.fmt = fmt;
1341
+ exports.getConfig = getConfig;
1342
+ exports.mul = mul;
1343
+ exports.mulStr = mulStr;
1344
+ exports.neg = neg;
1345
+ exports.parse = parse;
1346
+ exports.rawDiv = div$1;
1347
+ exports.resetConfig = resetConfig;
1348
+ exports.roundBanker = roundBanker;
1349
+ exports.roundCeil = roundCeil;
1350
+ exports.roundHalfUp = roundHalfUp;
1351
+ exports.setConfig = setConfig;
1352
+ exports.sub = sub;
1353
+ exports.subStr = subStr;
1354
+ exports.truncate = truncate;