Last active
March 21, 2026 11:50
-
-
Save qntm/5c3d90cb3c5673f8042651dca753991d to your computer and use it in GitHub Desktop.
Get all the decimal digits of any JavaScript float
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const SIGN_BITS = 1n | |
| const EXPONENT_BITS = 11n | |
| const MANTISSA_BITS = 52n | |
| const BIAS = 1023n | |
| export const stringify = value => { | |
| if (typeof value !== 'number') { | |
| throw Error('Not a number') | |
| } | |
| const dataView = new DataView(new ArrayBuffer(8)) | |
| dataView.setFloat64(0, value) | |
| const bigUint64 = dataView.getBigUint64(0) | |
| const mantissaBits = (bigUint64 >> 0n) & ((1n << MANTISSA_BITS) - 1n) | |
| const exponentBits = (bigUint64 >> MANTISSA_BITS) & ((1n << EXPONENT_BITS) - 1n) | |
| const signBits = (bigUint64 >> (MANTISSA_BITS + EXPONENT_BITS)) & ((1n << SIGN_BITS) - 1n) | |
| const sign = signBits === 0b0n ? '' : '-' | |
| if (exponentBits === ((1n << EXPONENT_BITS) - 1n)) { | |
| if (mantissaBits === 0n) { | |
| return sign + 'Infinity' | |
| } | |
| return 'NaN' | |
| } | |
| const isSubnormal = exponentBits === 0b0n | |
| // So as to keep this in integers, multiply the fraction by 2 ** 52 while subtracting | |
| // that same power from the exponent | |
| const m = ((isSubnormal ? 0n : 1n) << MANTISSA_BITS) + mantissaBits | |
| const e = (isSubnormal ? 1n : exponentBits) - BIAS - MANTISSA_BITS | |
| if (e >= 0n) { | |
| // Pure integers, no problem | |
| return sign + String(m << e) | |
| } | |
| // Multiply by a large enough power of 10 that all possible decimal digits are preserved | |
| // when we then divide by the power of 2 | |
| const power10 = 10n ** -e | |
| const f = (m * power10) >> -e | |
| const pre = f / power10 | |
| const post = f % power10 | |
| if (post === 0n) { | |
| return sign + String(pre) | |
| } | |
| return sign + String(pre) + '.' + String(post).padStart(Number(-e), '0').replace(/0+$/, '') | |
| } | |
| export const parse = str => { | |
| const value = Number(str) | |
| if (stringify(value) === str) { | |
| return value | |
| } | |
| throw Error('Parse failed') | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| An alternative implementation which wrangles the float directly | |
| to extract the digits after the decimal point. | |
| I'm undecided about the relative virtues of the two implementations... | |
| the clinching factor may be performance (neither implementation is optimised yet) | |
| */ | |
| const NUM_DIGITS = -Math.log2(Number.MIN_VALUE) | |
| export const stringify = value => { | |
| if (typeof value !== 'number') { | |
| throw Error('Not a number') | |
| } | |
| if (!Number.isFinite(value)) { | |
| return String(value) | |
| } | |
| const sign = value < 0 || Object.is(value, -0) ? '-' : '' | |
| value = Math.abs(value) | |
| const a = Math.trunc(value) | |
| if (value === a) { | |
| // Pure integers, no problem | |
| return `${sign}${BigInt(a)}` | |
| } | |
| // The fractional part is a value x/2^1074, where x is an integer with a minimum value of 1. | |
| // But what is x? We can't just multiply the fraction directly by 2^1074 because this results in overflow. | |
| // Do it in two stages | |
| value -= a | |
| value *= 2 ** (NUM_DIGITS / 2) | |
| const b = Math.trunc(value) | |
| value -= b | |
| value *= 2 ** (NUM_DIGITS / 2) | |
| const c = Math.trunc(value) | |
| const x = BigInt(b) * BigInt(2 ** (NUM_DIGITS / 2)) + BigInt(c) | |
| // x/2^1074 = y/10^1074 | |
| // y is suitable for serialisation in base 10 | |
| const y = x * (10n / 2n) ** BigInt(NUM_DIGITS) | |
| return `${sign}${BigInt(a)}.${String(y).padStart(NUM_DIGITS, '0').replace(/0+$/, '')}` | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Examples: