(n: bigint)
| 196 | |
| 197 | /** Count the number of decimal digits in a bigint (absolute value). */ |
| 198 | export function bigintDigits(n: bigint): number { |
| 199 | if (n === 0n) return 1; |
| 200 | if (n < 0n) n = -n; |
| 201 | // Fast path: fits in a Number (< 2^53), so `Number(n)` is exact. Resolve the |
| 202 | // digit count with an exact float comparison ladder (a 4-deep binary search |
| 203 | // over the 16 possible lengths) rather than `Math.floor(Math.log10(x)) + 1`. |
| 204 | // The `log10` form is not only a libm call but *wrong* at a power-of-ten |
| 205 | // boundary: `Math.log10(999999999999999) === 15` rounds up, so it returned 16 |
| 206 | // for the fifteen-nines value (and 999999999999998), over-counting by one and |
| 207 | // corrupting every consumer that derives an order of magnitude from the count |
| 208 | // (`cmp`'s magnitude early-out could invert an ordering; `toPrecision(15)` of |
| 209 | // a genuine 15-digit value rounded it to 1e15). Every power 10^0…10^15 is an |
| 210 | // exact double (10^15 < 2^53) and `x` is exact, so each comparison is exact — |
| 211 | // the ladder is correct by construction and needs no boundary fix-up. It is |
| 212 | // also faster than the old path (no `log10`): ~17–34% in an A/B micro-bench. |
| 213 | if (n < 0x20000000000000n) { |
| 214 | const x = Number(n); |
| 215 | if (x < 1e8) { |
| 216 | if (x < 1e4) return x < 1e2 ? (x < 1e1 ? 1 : 2) : x < 1e3 ? 3 : 4; |
| 217 | return x < 1e6 ? (x < 1e5 ? 5 : 6) : x < 1e7 ? 7 : 8; |
| 218 | } |
| 219 | if (x < 1e12) return x < 1e10 ? (x < 1e9 ? 9 : 10) : x < 1e11 ? 11 : 12; |
| 220 | return x < 1e14 ? (x < 1e13 ? 13 : 14) : x < 1e15 ? 15 : 16; |
| 221 | } |
| 222 | // Large bigints: seed the decimal digit count from the hex-string length. |
| 223 | // `n.toString(16).length` is a single near-linear native base-16 conversion |
| 224 | // (hex being a power of 2, it is far cheaper than a base-10 stringification) |
| 225 | // and gives `bits/4` rounded up — i.e. an upper bound on the bit length, high |
| 226 | // by at most the 3 bits the top nibble may leave unused. That is ≤ ~1 decimal |
| 227 | // digit of slack, which the cached-pow10 boundary walk (0–2 steps) settles |
| 228 | // exactly. Replaces the doubling + binary-search bit-length scan, whose |
| 229 | // ~2·log2(bits) per-step `tmp >> BigInt(shift)` calls each allocated a fresh |
| 230 | // full-width bigint. Byte-identical result; ~2–4× faster in the 30–500-digit |
| 231 | // working-precision range that dominates div/cmp operand sizing. |
| 232 | const bits = n.toString(16).length * 4; |
| 233 | let approx = Math.ceil(bits * 0.30102999566398); |
| 234 | while (n < pow10(approx - 1)) approx--; |
| 235 | while (n >= pow10(approx)) approx++; |
| 236 | return approx; |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Extra argument-reduction depth for `fpexp`, as a multiple of √bits. |
no test coverage detected