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