@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
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// Precision math: internally represents decimal numbers using BigInt
|
|
2
|
+
// A number = sign * digits / 10^exp (exp >= 0 denotes the number of decimal places)
|
|
3
|
+
|
|
4
|
+
/** Internal decimal representation: a number = `sign * digits / 10^exp` */
|
|
5
|
+
export interface IDecimal {
|
|
6
|
+
/** Sign: `1` for positive, `-1` for negative. */
|
|
7
|
+
sign: 1 | -1
|
|
8
|
+
/** Significant digits as a non-negative integer (decimal point removed). */
|
|
9
|
+
digits: bigint
|
|
10
|
+
/** Number of decimal places (negative exponent of `10`). */
|
|
11
|
+
exp: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TEN = 10n
|
|
15
|
+
|
|
16
|
+
// Cache powers of 10 to avoid repeatedly constructing large BigInts like 10^precision in division
|
|
17
|
+
// (inspired by decimal.js's "precompute constants, no recomputation" approach; grows on demand, reused across calls)
|
|
18
|
+
const POW10: bigint[] = [1n]
|
|
19
|
+
const pow10 = (n: number): bigint => {
|
|
20
|
+
for (let i = POW10.length; i <= n; i++) POW10[i] = POW10[i - 1]! * TEN
|
|
21
|
+
return POW10[n]!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ───── Module-level constant regexes ─────
|
|
25
|
+
const RE_SCI = /^([+-]?\d+(?:\.\d+)?)e([+-]?\d+)$/i
|
|
26
|
+
const RE_NUM = /^\d+(?:\.\d+)?$/
|
|
27
|
+
const RE_TRAIL_ZERO = /0+$/
|
|
28
|
+
|
|
29
|
+
/** Shifts the decimal point of `"1.23"` left or right by `n` positions → string. */
|
|
30
|
+
const shiftDecimalPoint = (s: string, n: number): string => {
|
|
31
|
+
let sign = ''
|
|
32
|
+
if (s.startsWith('-')) {
|
|
33
|
+
sign = '-'
|
|
34
|
+
s = s.slice(1)
|
|
35
|
+
} else if (s.startsWith('+')) {
|
|
36
|
+
s = s.slice(1)
|
|
37
|
+
}
|
|
38
|
+
const dot = s.indexOf('.')
|
|
39
|
+
const allDigits = dot === -1 ? s : s.slice(0, dot) + s.slice(dot + 1)
|
|
40
|
+
const dotPos = (dot === -1 ? s.length : dot) + n
|
|
41
|
+
if (dotPos <= 0) return `${sign}0.${'0'.repeat(-dotPos)}${allDigits}`
|
|
42
|
+
if (dotPos >= allDigits.length) return sign + allDigits + '0'.repeat(dotPos - allDigits.length)
|
|
43
|
+
return `${sign}${allDigits.slice(0, dotPos)}.${allDigits.slice(dotPos)}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parses a string, number, or bigint into the internal {@link IDecimal} representation.
|
|
48
|
+
*
|
|
49
|
+
* Supports standard decimals (`"1.23"`, `-0.5`) and scientific notation (`"1.2e-3"`).
|
|
50
|
+
*
|
|
51
|
+
* @param input Value to parse, e.g. `"3.14"`, `42`, `-1n`, `"1e10"`
|
|
52
|
+
* @returns Parsed {@link IDecimal} (`digits` is always >= 0; sign is carried by `sign`)
|
|
53
|
+
* @throws When the string is not a valid number
|
|
54
|
+
* @example
|
|
55
|
+
* parse('1.23') // { sign: 1, digits: 123n, exp: 2 }
|
|
56
|
+
* parse('-5') // { sign: -1, digits: 5n, exp: 0 }
|
|
57
|
+
*/
|
|
58
|
+
export const parse = (input: string | number | bigint): IDecimal => {
|
|
59
|
+
if (typeof input === 'bigint') return { sign: input < 0n ? -1 : 1, digits: input < 0n ? -input : input, exp: 0 }
|
|
60
|
+
let s = String(input).trim()
|
|
61
|
+
if (s === '' || s === '-' || s === '+') throw new Error(`Cannot parse number: "${input}"`)
|
|
62
|
+
const expMatch = RE_SCI.exec(s)
|
|
63
|
+
if (expMatch) {
|
|
64
|
+
const base = expMatch[1]!
|
|
65
|
+
const e = Number.parseInt(expMatch[2]!, 10)
|
|
66
|
+
s = shiftDecimalPoint(base, e)
|
|
67
|
+
}
|
|
68
|
+
let sign: 1 | -1 = 1
|
|
69
|
+
if (s.startsWith('+')) {
|
|
70
|
+
s = s.slice(1)
|
|
71
|
+
} else if (s.startsWith('-')) {
|
|
72
|
+
sign = -1
|
|
73
|
+
s = s.slice(1)
|
|
74
|
+
}
|
|
75
|
+
if (!RE_NUM.test(s)) throw new Error(`Cannot parse number: "${input}"`)
|
|
76
|
+
const dot = s.indexOf('.')
|
|
77
|
+
const digitStr = dot === -1 ? s : s.slice(0, dot) + s.slice(dot + 1)
|
|
78
|
+
const exp = dot === -1 ? 0 : s.length - dot - 1
|
|
79
|
+
const digits = BigInt(digitStr || '0')
|
|
80
|
+
if (digits === 0n) return { sign: 1, digits: 0n, exp: 0 }
|
|
81
|
+
return { sign, digits, exp }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Converts an {@link IDecimal} back to a canonical decimal string (trailing zeros are stripped automatically).
|
|
86
|
+
*
|
|
87
|
+
* @param d The {@link IDecimal} to format
|
|
88
|
+
* @returns Canonical string; zero is always returned as `"0"`
|
|
89
|
+
* @example
|
|
90
|
+
* format({ sign: 1, digits: 1230n, exp: 3 }) // '1.23'
|
|
91
|
+
*/
|
|
92
|
+
export const format = (d: IDecimal): string => {
|
|
93
|
+
if (d.digits === 0n) return '0'
|
|
94
|
+
let raw = d.digits.toString()
|
|
95
|
+
if (d.exp > 0) {
|
|
96
|
+
if (raw.length <= d.exp) raw = '0'.repeat(d.exp - raw.length + 1) + raw
|
|
97
|
+
const intPart = raw.slice(0, raw.length - d.exp)
|
|
98
|
+
const fracPart = raw.slice(raw.length - d.exp).replace(RE_TRAIL_ZERO, '')
|
|
99
|
+
raw = fracPart ? `${intPart}.${fracPart}` : intPart
|
|
100
|
+
}
|
|
101
|
+
return (d.sign < 0 ? '-' : '') + raw
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Aligns the digits of two numbers to the same exp (using the larger of the two). */
|
|
105
|
+
const align = (a: IDecimal, b: IDecimal): { aN: bigint, bN: bigint, exp: number } => {
|
|
106
|
+
if (a.exp === b.exp) return { aN: a.digits, bN: b.digits, exp: a.exp }
|
|
107
|
+
if (a.exp > b.exp) return { aN: a.digits, bN: b.digits * TEN ** BigInt(a.exp - b.exp), exp: a.exp }
|
|
108
|
+
return { aN: a.digits * TEN ** BigInt(b.exp - a.exp), bN: b.digits, exp: b.exp }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fromBig = (n: bigint, exp: number): IDecimal => {
|
|
112
|
+
if (n === 0n) return { sign: 1, digits: 0n, exp: 0 }
|
|
113
|
+
return { sign: n < 0n ? -1 : 1, digits: n < 0n ? -n : n, exp }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ───── Fast path using integer-scaled numbers ─────
|
|
117
|
+
// When both operands can be represented as safe integer mantissas (value within 2^53, decimal places <= 15),
|
|
118
|
+
// use integer arithmetic on `number` for near-native speed; otherwise return null and fall back to the BigInt implementation below.
|
|
119
|
+
// Critical: scaling is done only as "integer × 10^k" — never multiply by a floating-point decimal (e.g. 0.29 * 100 = 28.999… would be wrong).
|
|
120
|
+
const MAX_SAFE = Number.MAX_SAFE_INTEGER
|
|
121
|
+
const POW10F = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15]
|
|
122
|
+
|
|
123
|
+
interface IFastNum {
|
|
124
|
+
/** Safe integer mantissa (with sign). */
|
|
125
|
+
int: number
|
|
126
|
+
/** Number of decimal places; actual value = int / 10^scale. */
|
|
127
|
+
scale: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 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. */
|
|
131
|
+
const parseFast = (input: string | number | bigint): IFastNum | null => {
|
|
132
|
+
const s = typeof input === 'string' ? input : String(input)
|
|
133
|
+
let i = 0
|
|
134
|
+
let neg = false
|
|
135
|
+
const c0 = s.charCodeAt(0)
|
|
136
|
+
if (c0 === 45) { // '-'
|
|
137
|
+
neg = true
|
|
138
|
+
i = 1
|
|
139
|
+
} else if (c0 === 43) { // '+'
|
|
140
|
+
i = 1
|
|
141
|
+
}
|
|
142
|
+
let hasDot = false
|
|
143
|
+
let scale = 0
|
|
144
|
+
let digits = ''
|
|
145
|
+
for (; i < s.length; i++) {
|
|
146
|
+
const c = s.charCodeAt(i)
|
|
147
|
+
if (c === 46) { // '.'
|
|
148
|
+
if (hasDot || digits === '') return null // multiple dots or no integer part (".5") → fall back to strict parse
|
|
149
|
+
hasDot = true
|
|
150
|
+
} else if (c >= 48 && c <= 57) { // '0'..'9'
|
|
151
|
+
digits += s[i]
|
|
152
|
+
if (hasDot) scale++
|
|
153
|
+
} else {
|
|
154
|
+
return null // 'e' (scientific notation), whitespace, or illegal character → fall back
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (digits === '' || (hasDot && scale === 0)) return null // empty or trailing dot ("1.") → fall back
|
|
158
|
+
if (digits.length > 15 || scale >= POW10F.length) return null // exceeds safe integer range or too many decimal places → fall back
|
|
159
|
+
const int = Number(digits)
|
|
160
|
+
return { int: neg ? -int : int, scale }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Formats `int / 10^scale` as a canonical string, output identical to {@link format} (trailing zeros stripped, zero returns `'0'`). */
|
|
164
|
+
const fmtIntFast = (int: number, scale: number): string => {
|
|
165
|
+
if (int === 0) return '0'
|
|
166
|
+
const neg = int < 0
|
|
167
|
+
let s = (neg ? -int : int).toString()
|
|
168
|
+
if (scale === 0) return neg ? `-${s}` : s
|
|
169
|
+
if (s.length <= scale) s = '0'.repeat(scale - s.length + 1) + s
|
|
170
|
+
const cut = s.length - scale
|
|
171
|
+
let end = s.length
|
|
172
|
+
while (end > cut && s.charCodeAt(end - 1) === 48) end-- // strip trailing zeros
|
|
173
|
+
const out = end === cut ? s.slice(0, cut) : `${s.slice(0, cut)}.${s.slice(cut, end)}`
|
|
174
|
+
return neg ? `-${out}` : out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Fast path for addition/subtraction: `op` is `1` (add) or `-1` (subtract); returns `null` when unavailable. */
|
|
178
|
+
const fastAddSub = (a: string | number | bigint, b: string | number | bigint, op: 1 | -1): string | null => {
|
|
179
|
+
const pa = parseFast(a)
|
|
180
|
+
if (pa === null) return null
|
|
181
|
+
const pb = parseFast(b)
|
|
182
|
+
if (pb === null) return null
|
|
183
|
+
const scale = pa.scale > pb.scale ? pa.scale : pb.scale
|
|
184
|
+
const ia = pa.int * POW10F[scale - pa.scale]!
|
|
185
|
+
const ib = pb.int * POW10F[scale - pb.scale]!
|
|
186
|
+
if (ia > MAX_SAFE || ia < -MAX_SAFE || ib > MAX_SAFE || ib < -MAX_SAFE) return null
|
|
187
|
+
const r = ia + op * ib
|
|
188
|
+
if (r > MAX_SAFE || r < -MAX_SAFE) return null
|
|
189
|
+
return fmtIntFast(r, scale)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Fast path for multiplication; returns `null` when unavailable. */
|
|
193
|
+
const fastMul = (a: string | number | bigint, b: string | number | bigint): string | null => {
|
|
194
|
+
const pa = parseFast(a)
|
|
195
|
+
if (pa === null) return null
|
|
196
|
+
const pb = parseFast(b)
|
|
197
|
+
if (pb === null) return null
|
|
198
|
+
const r = pa.int * pb.int
|
|
199
|
+
if (r > MAX_SAFE || r < -MAX_SAFE) return null
|
|
200
|
+
return fmtIntFast(r, pa.scale + pb.scale)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ───── Public arithmetic ─────
|
|
204
|
+
/**
|
|
205
|
+
* High-precision addition `a + b`, computed entirely with BigInt — no floating-point errors.
|
|
206
|
+
*
|
|
207
|
+
* @param a Augend
|
|
208
|
+
* @param b Addend
|
|
209
|
+
* @returns Canonical string of the sum
|
|
210
|
+
* @example
|
|
211
|
+
* add('0.1', '0.2') // '0.3'
|
|
212
|
+
*/
|
|
213
|
+
export const add = (a: string | number | bigint, b: string | number | bigint): string => {
|
|
214
|
+
const fast = fastAddSub(a, b, 1)
|
|
215
|
+
if (fast !== null) return fast
|
|
216
|
+
const da = parse(a)
|
|
217
|
+
const db = parse(b)
|
|
218
|
+
const { aN, bN, exp } = align(da, db)
|
|
219
|
+
const r = BigInt(da.sign) * aN + BigInt(db.sign) * bN
|
|
220
|
+
return format(fromBig(r, exp))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* High-precision subtraction `a - b`.
|
|
225
|
+
*
|
|
226
|
+
* @param a Minuend
|
|
227
|
+
* @param b Subtrahend
|
|
228
|
+
* @returns Canonical string of the difference
|
|
229
|
+
* @example
|
|
230
|
+
* sub('0.3', '0.1') // '0.2'
|
|
231
|
+
*/
|
|
232
|
+
export const sub = (a: string | number | bigint, b: string | number | bigint): string => {
|
|
233
|
+
const fast = fastAddSub(a, b, -1)
|
|
234
|
+
if (fast !== null) return fast
|
|
235
|
+
const da = parse(a)
|
|
236
|
+
const db = parse(b)
|
|
237
|
+
const { aN, bN, exp } = align(da, db)
|
|
238
|
+
const r = BigInt(da.sign) * aN - BigInt(db.sign) * bN
|
|
239
|
+
return format(fromBig(r, exp))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* High-precision multiplication `a * b`.
|
|
244
|
+
*
|
|
245
|
+
* @param a Factor
|
|
246
|
+
* @param b Factor
|
|
247
|
+
* @returns Canonical string of the product
|
|
248
|
+
* @example
|
|
249
|
+
* mul('0.1', '0.2') // '0.02'
|
|
250
|
+
*/
|
|
251
|
+
export const mul = (a: string | number | bigint, b: string | number | bigint): string => {
|
|
252
|
+
const fast = fastMul(a, b)
|
|
253
|
+
if (fast !== null) return fast
|
|
254
|
+
const da = parse(a)
|
|
255
|
+
const db = parse(b)
|
|
256
|
+
const sign = (da.sign * db.sign) as 1 | -1
|
|
257
|
+
const digits = da.digits * db.digits
|
|
258
|
+
const exp = da.exp + db.exp
|
|
259
|
+
if (digits === 0n) return '0'
|
|
260
|
+
return format({ sign, digits, exp })
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* High-precision division `a / b`, result rounded to `precision` decimal places (half-up).
|
|
265
|
+
*
|
|
266
|
+
* @param a Dividend
|
|
267
|
+
* @param b Divisor
|
|
268
|
+
* @param precision Maximum decimal places to retain, defaults to `50`
|
|
269
|
+
* @returns Canonical string of the quotient
|
|
270
|
+
* @throws When the divisor is zero
|
|
271
|
+
* @example
|
|
272
|
+
* div('1', '3') // '0.333...' (up to 50 places)
|
|
273
|
+
* div('1', '3', 4) // '0.3333'
|
|
274
|
+
*/
|
|
275
|
+
export const div = (a: string | number | bigint, b: string | number | bigint, precision: number = 50): string => {
|
|
276
|
+
const da = parse(a)
|
|
277
|
+
const db = parse(b)
|
|
278
|
+
if (db.digits === 0n) throw new Error('Division by zero')
|
|
279
|
+
if (da.digits === 0n) return '0'
|
|
280
|
+
const sign = (da.sign * db.sign) as 1 | -1
|
|
281
|
+
// Let result = a / b = (da.digits * 10^-da.exp) / (db.digits * 10^-db.exp)
|
|
282
|
+
// = (da.digits / db.digits) * 10^(db.exp - da.exp)
|
|
283
|
+
// Fast path: if the division is exact the result is a finite decimal; return it directly
|
|
284
|
+
// without scaling to precision+1 digits and doing large-integer division + rounding
|
|
285
|
+
// (analogous to decimal.js "early exit when remainder is zero")
|
|
286
|
+
if (da.digits % db.digits === 0n) {
|
|
287
|
+
const q = da.digits / db.digits
|
|
288
|
+
const e = da.exp - db.exp // result = q * 10^(-e)
|
|
289
|
+
const d: IDecimal = e >= 0 ? { sign, digits: q, exp: e } : { sign, digits: q * pow10(-e), exp: 0 }
|
|
290
|
+
return format(d)
|
|
291
|
+
}
|
|
292
|
+
// We want q = result * 10^(precision + 1) ⇒ shift = precision + 1 + db.exp - da.exp
|
|
293
|
+
const shift = precision + 1 + db.exp - da.exp
|
|
294
|
+
let num = da.digits
|
|
295
|
+
let den = db.digits
|
|
296
|
+
if (shift >= 0) num = num * pow10(shift)
|
|
297
|
+
else den = den * pow10(-shift)
|
|
298
|
+
const q = num / den
|
|
299
|
+
// q now represents result * 10^(precision+1); inspect the last digit to apply rounding
|
|
300
|
+
const rounded = q % TEN >= 5n ? q / TEN + 1n : q / TEN
|
|
301
|
+
return format(fromBig(sign === 1 ? rounded : -rounded, precision))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ───── Utilities ─────
|
|
305
|
+
/**
|
|
306
|
+
* Compares two numbers numerically.
|
|
307
|
+
*
|
|
308
|
+
* @param a Left operand
|
|
309
|
+
* @param b Right operand
|
|
310
|
+
* @returns `1` if `a > b`, `-1` if `a < b`, `0` if equal
|
|
311
|
+
* @example
|
|
312
|
+
* cmp('0.1', '0.2') // -1
|
|
313
|
+
* cmp('2', '2.0') // 0
|
|
314
|
+
*/
|
|
315
|
+
export const cmp = (a: string | number | bigint, b: string | number | bigint): number => {
|
|
316
|
+
const da = parse(a)
|
|
317
|
+
const db = parse(b)
|
|
318
|
+
if (da.digits === 0n && db.digits === 0n) return 0
|
|
319
|
+
if (da.sign !== db.sign) return da.sign < db.sign ? -1 : 1
|
|
320
|
+
const { aN, bN } = align(da, db)
|
|
321
|
+
if (aN === bN) return 0
|
|
322
|
+
const signed = da.sign === 1 ? (aN > bN ? 1 : -1) : (aN > bN ? -1 : 1)
|
|
323
|
+
return signed
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Returns the negation `-a`.
|
|
328
|
+
*
|
|
329
|
+
* @param a Input value
|
|
330
|
+
* @returns Canonical string of the negation (`0` always returns `'0'`)
|
|
331
|
+
* @example
|
|
332
|
+
* neg('1.5') // '-1.5'
|
|
333
|
+
* neg('-2') // '2'
|
|
334
|
+
*/
|
|
335
|
+
export const neg = (a: string | number | bigint): string => {
|
|
336
|
+
const d = parse(a)
|
|
337
|
+
if (d.digits === 0n) return '0'
|
|
338
|
+
return format({ ...d, sign: -d.sign as 1 | -1 })
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Returns the absolute value `|a|`.
|
|
343
|
+
*
|
|
344
|
+
* @param a Input value
|
|
345
|
+
* @returns Canonical string of the absolute value
|
|
346
|
+
* @example
|
|
347
|
+
* abs('-3.14') // '3.14'
|
|
348
|
+
*/
|
|
349
|
+
export const abs = (a: string | number | bigint): string => {
|
|
350
|
+
const d = parse(a)
|
|
351
|
+
return format({ ...d, sign: 1 })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Truncates to N decimal places by discarding excess digits — **no rounding**.
|
|
356
|
+
*
|
|
357
|
+
* @param a Input value
|
|
358
|
+
* @param decimals Number of decimal places to keep
|
|
359
|
+
* @returns Canonical string after truncation
|
|
360
|
+
* @example
|
|
361
|
+
* truncate('1.2349', 2) // '1.23'
|
|
362
|
+
* truncate('1.99', 0) // '1'
|
|
363
|
+
*/
|
|
364
|
+
export const truncate = (a: string | number | bigint, decimals: number): string => {
|
|
365
|
+
const d = parse(a)
|
|
366
|
+
if (d.exp <= decimals) return format(d)
|
|
367
|
+
const drop = d.exp - decimals
|
|
368
|
+
const newDigits = d.digits / TEN ** BigInt(drop)
|
|
369
|
+
if (newDigits === 0n) return '0'
|
|
370
|
+
return format({ sign: d.sign, digits: newDigits, exp: decimals })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Rounds to N decimal places using half-up rounding (rounds up when digit `>= 0.5`).
|
|
375
|
+
*
|
|
376
|
+
* @param a Input value
|
|
377
|
+
* @param decimals Number of decimal places to keep
|
|
378
|
+
* @returns Canonical string after rounding
|
|
379
|
+
* @example
|
|
380
|
+
* roundHalfUp('1.235', 2) // '1.24'
|
|
381
|
+
* roundHalfUp('1.234', 2) // '1.23'
|
|
382
|
+
*/
|
|
383
|
+
export const roundHalfUp = (a: string | number | bigint, decimals: number): string => {
|
|
384
|
+
const d = parse(a)
|
|
385
|
+
if (d.exp <= decimals) return format(d)
|
|
386
|
+
const drop = d.exp - decimals
|
|
387
|
+
const divisor = TEN ** BigInt(drop)
|
|
388
|
+
const halved = TEN ** BigInt(drop - 1) * 5n
|
|
389
|
+
const remainder = d.digits % divisor
|
|
390
|
+
let q = d.digits / divisor
|
|
391
|
+
if (remainder >= halved) q += 1n
|
|
392
|
+
if (q === 0n) return '0'
|
|
393
|
+
return format({ sign: d.sign, digits: q, exp: decimals })
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Rounds away from zero to N decimal places (rounds up whenever there is any remainder).
|
|
398
|
+
*
|
|
399
|
+
* @param a Input value
|
|
400
|
+
* @param decimals Number of decimal places to keep
|
|
401
|
+
* @returns Canonical string after rounding
|
|
402
|
+
* @example
|
|
403
|
+
* roundCeil('1.231', 2) // '1.24'
|
|
404
|
+
* roundCeil('-1.231', 2) // '-1.24'
|
|
405
|
+
*/
|
|
406
|
+
export const roundCeil = (a: string | number | bigint, decimals: number): string => {
|
|
407
|
+
const d = parse(a)
|
|
408
|
+
if (d.exp <= decimals) return format(d)
|
|
409
|
+
const drop = d.exp - decimals
|
|
410
|
+
const divisor = TEN ** BigInt(drop)
|
|
411
|
+
const remainder = d.digits % divisor
|
|
412
|
+
let q = d.digits / divisor
|
|
413
|
+
if (remainder !== 0n) q += 1n
|
|
414
|
+
if (q === 0n) return '0'
|
|
415
|
+
return format({ sign: d.sign, digits: q, exp: decimals })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Banker's rounding (round half to even): when the discarded portion is exactly `0.5`,
|
|
420
|
+
* rounds to the nearest even digit.
|
|
421
|
+
*
|
|
422
|
+
* @param a Input value
|
|
423
|
+
* @param decimals Number of decimal places to keep
|
|
424
|
+
* @returns Canonical string after rounding
|
|
425
|
+
* @example
|
|
426
|
+
* roundBanker('0.5', 0) // '0'
|
|
427
|
+
* roundBanker('1.5', 0) // '2'
|
|
428
|
+
* roundBanker('2.5', 0) // '2'
|
|
429
|
+
*/
|
|
430
|
+
export const roundBanker = (a: string | number | bigint, decimals: number): string => {
|
|
431
|
+
const d = parse(a)
|
|
432
|
+
if (d.exp <= decimals) return format(d)
|
|
433
|
+
const drop = d.exp - decimals
|
|
434
|
+
const divisor = TEN ** BigInt(drop)
|
|
435
|
+
const halved = TEN ** BigInt(drop - 1) * 5n
|
|
436
|
+
const remainder = d.digits % divisor
|
|
437
|
+
let q = d.digits / divisor
|
|
438
|
+
if (remainder > halved) q += 1n
|
|
439
|
+
else if (remainder === halved && q % 2n === 1n) q += 1n
|
|
440
|
+
if (q === 0n) return '0'
|
|
441
|
+
return format({ sign: d.sign, digits: q, exp: decimals })
|
|
442
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Standalone arithmetic functions:
|
|
2
|
+
// add/sub/mul/div → return number (convenient for interop with native JS numbers)
|
|
3
|
+
// addStr/subStr/mulStr/divStr → return string (preserves full precision)
|
|
4
|
+
// Errors (invalid arguments, etc.) are thrown directly; use the calc expression form if you need a fallback
|
|
5
|
+
|
|
6
|
+
import type { IGlobalConfig, IPrecisionOption } from './config'
|
|
7
|
+
import { configWithPrecision } from './config'
|
|
8
|
+
import * as precision from './precision'
|
|
9
|
+
|
|
10
|
+
type Val = string | number | bigint
|
|
11
|
+
|
|
12
|
+
const reduce = (op: (a: string, b: Val) => string, args: Val[]): string => {
|
|
13
|
+
if (args.length === 0) return '0'
|
|
14
|
+
let r = String(args[0])
|
|
15
|
+
for (let i = 1; i < args.length; i++) r = op(r, args[i]!)
|
|
16
|
+
return r
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 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). */
|
|
20
|
+
const splitPrecision = (args: Array<Val | IPrecisionOption>): [Val[], IPrecisionOption?] => {
|
|
21
|
+
const last = args.at(-1)
|
|
22
|
+
if (last != null && typeof last === 'object') {
|
|
23
|
+
return [args.slice(0, -1) as Val[], last as IPrecisionOption]
|
|
24
|
+
}
|
|
25
|
+
return [args as Val[], undefined]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Division using the precision from the given config (shared by the default {@link div} export and per-call precision entry points). */
|
|
29
|
+
export const divWith = (cfg: IGlobalConfig, ...args: Val[]): number =>
|
|
30
|
+
Number(reduce((a, b) => precision.div(a, b, cfg._precision), args))
|
|
31
|
+
/** Division using the given config precision, string-returning variant (shared implementation). */
|
|
32
|
+
export const divStrWith = (cfg: IGlobalConfig, ...args: Val[]): string =>
|
|
33
|
+
reduce((a, b) => precision.div(a, b, cfg._precision), args)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* High-precision addition, result converted to `number` (convenient for interop with native numbers).
|
|
37
|
+
*
|
|
38
|
+
* @param args Any number of addends, accumulated left to right
|
|
39
|
+
* @returns Sum (`number`)
|
|
40
|
+
* @example
|
|
41
|
+
* add(0.1, 0.2) // 0.3
|
|
42
|
+
* add(1, 2, 3, 4) // 10
|
|
43
|
+
*/
|
|
44
|
+
export const add = (...args: Val[]): number => Number(reduce(precision.add, args))
|
|
45
|
+
/**
|
|
46
|
+
* High-precision subtraction, result as `number`: `args[0] - args[1] - args[2] ...`.
|
|
47
|
+
*
|
|
48
|
+
* @param args Minuend followed by subtrahends
|
|
49
|
+
* @returns Difference (`number`)
|
|
50
|
+
* @example
|
|
51
|
+
* sub(0.3, 0.1) // 0.2
|
|
52
|
+
*/
|
|
53
|
+
export const sub = (...args: Val[]): number => Number(reduce(precision.sub, args))
|
|
54
|
+
/**
|
|
55
|
+
* High-precision multiplication, result as `number`, factors multiplied left to right.
|
|
56
|
+
*
|
|
57
|
+
* @param args Any number of factors
|
|
58
|
+
* @returns Product (`number`)
|
|
59
|
+
* @example
|
|
60
|
+
* mul(0.1, 0.2) // 0.02
|
|
61
|
+
*/
|
|
62
|
+
export const mul = (...args: Val[]): number => Number(reduce(precision.mul, args))
|
|
63
|
+
/**
|
|
64
|
+
* High-precision division, result as `number`: `args[0] / args[1] / args[2] ...`.
|
|
65
|
+
*
|
|
66
|
+
* Precision defaults to the global `_precision`; pass `{ _precision }` as the **last** argument
|
|
67
|
+
* to override it for this call only (does not affect the global config).
|
|
68
|
+
*
|
|
69
|
+
* @param args Dividend and divisors, with an optional {@link IPrecisionOption} at the end
|
|
70
|
+
* @returns Quotient (`number`)
|
|
71
|
+
* @example
|
|
72
|
+
* div(1, 3) // 0.333... (global precision)
|
|
73
|
+
* div(100, 3, { _precision: 5 }) // 33.33333
|
|
74
|
+
*/
|
|
75
|
+
export function div(...args: Val[]): number
|
|
76
|
+
export function div(...args: [...Val[], IPrecisionOption]): number
|
|
77
|
+
export function div(...args: Array<Val | IPrecisionOption>): number {
|
|
78
|
+
const [values, opt] = splitPrecision(args)
|
|
79
|
+
return divWith(configWithPrecision(opt), ...values)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* High-precision addition, result returned as `string` (no precision loss; suitable for monetary values).
|
|
84
|
+
*
|
|
85
|
+
* @param args Any number of addends
|
|
86
|
+
* @returns Sum (`string`)
|
|
87
|
+
* @example
|
|
88
|
+
* addStr('0.1', '0.2') // '0.3'
|
|
89
|
+
*/
|
|
90
|
+
export const addStr = (...args: Val[]): string => reduce(precision.add, args)
|
|
91
|
+
/**
|
|
92
|
+
* High-precision subtraction, result as `string`: `args[0] - args[1] - ...`.
|
|
93
|
+
*
|
|
94
|
+
* @param args Minuend followed by subtrahends
|
|
95
|
+
* @returns Difference (`string`)
|
|
96
|
+
* @example
|
|
97
|
+
* subStr('0.3', '0.1') // '0.2'
|
|
98
|
+
*/
|
|
99
|
+
export const subStr = (...args: Val[]): string => reduce(precision.sub, args)
|
|
100
|
+
/**
|
|
101
|
+
* High-precision multiplication, result as `string`, factors multiplied left to right.
|
|
102
|
+
*
|
|
103
|
+
* @param args Any number of factors
|
|
104
|
+
* @returns Product (`string`)
|
|
105
|
+
* @example
|
|
106
|
+
* mulStr('0.1', '0.2') // '0.02'
|
|
107
|
+
*/
|
|
108
|
+
export const mulStr = (...args: Val[]): string => reduce(precision.mul, args)
|
|
109
|
+
/**
|
|
110
|
+
* High-precision division, result as `string`: `args[0] / args[1] / ...`.
|
|
111
|
+
*
|
|
112
|
+
* Precision defaults to the global `_precision`; pass `{ _precision }` as the **last** argument
|
|
113
|
+
* to override it for this call only (does not affect the global config).
|
|
114
|
+
*
|
|
115
|
+
* @param args Dividend and divisors, with an optional {@link IPrecisionOption} at the end
|
|
116
|
+
* @returns Quotient (`string`)
|
|
117
|
+
* @example
|
|
118
|
+
* divStr('1', '3') // '0.333...' (global precision)
|
|
119
|
+
* divStr('100', '3', { _precision: 5 }) // '33.33333'
|
|
120
|
+
*/
|
|
121
|
+
export function divStr(...args: Val[]): string
|
|
122
|
+
export function divStr(...args: [...Val[], IPrecisionOption]): string
|
|
123
|
+
export function divStr(...args: Array<Val | IPrecisionOption>): string {
|
|
124
|
+
const [values, opt] = splitPrecision(args)
|
|
125
|
+
return divStrWith(configWithPrecision(opt), ...values)
|
|
126
|
+
}
|