@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/LICENSE +21 -0
- package/README.md +115 -0
- package/README.zh.md +115 -0
- package/dist/index.cjs +1354 -0
- package/dist/index.d.cts +449 -0
- package/dist/index.d.mts +449 -0
- package/dist/index.mjs +1324 -0
- package/package.json +67 -0
- package/src/index.ts +31 -0
- package/src/utils/aggregate.ts +109 -0
- package/src/utils/calc.ts +100 -0
- package/src/utils/chain.ts +127 -0
- package/src/utils/config.ts +59 -0
- package/src/utils/format.ts +366 -0
- package/src/utils/parser.ts +310 -0
- package/src/utils/precision.ts +442 -0
- package/src/utils/standalone.ts +126 -0
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 };
|