Skip to content

Instantly share code, notes, and snippets.

@qntm
Last active March 21, 2026 11:50
Show Gist options
  • Select an option

  • Save qntm/5c3d90cb3c5673f8042651dca753991d to your computer and use it in GitHub Desktop.

Select an option

Save qntm/5c3d90cb3c5673f8042651dca753991d to your computer and use it in GitHub Desktop.
Get all the decimal digits of any JavaScript float
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')
}
/*
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+$/, '')}`
}
@qntm

qntm commented Dec 9, 2021 •

Copy link
Copy Markdown
Author

Examples:

import assert from 'node:assert/strict'
import { stringify, parse } from './xact.js'

assert.throws(() => stringify('abc'))
assert.equal(stringify(Infinity), 'Infinity')
assert.equal(stringify(-Infinity), '-Infinity')
assert.equal(stringify(NaN), 'NaN')
assert.equal(stringify(0), '0')
assert.equal(stringify(-0), '-0')
assert.equal(stringify(1), '1')
assert.equal(stringify(-34), '-34')
assert.equal(stringify(Number.MAX_VALUE), '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368')
assert.equal(stringify(0.5), '0.5')
assert.equal(stringify(0.1), '0.1000000000000000055511151231257827021181583404541015625')
assert.equal(stringify(0.2), '0.200000000000000011102230246251565404236316680908203125')
assert.equal(stringify(0.3), '0.299999999999999988897769753748434595763683319091796875')
assert.equal(stringify(.1 + .2), '0.3000000000000000444089209850062616169452667236328125')
assert.equal(stringify(Math.PI), '3.141592653589793115997963468544185161590576171875')
assert.equal(stringify(-78.3), '-78.2999999999999971578290569595992565155029296875')
assert.equal(stringify(Number.EPSILON), '0.0000000000000002220446049250313080847263336181640625')
assert.equal(stringify(Number.MIN_VALUE), '0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625')

assert.equal(parse('Infinity'), Infinity)
assert.equal(parse('-Infinity'), -Infinity)
assert.equal(parse('NaN'), NaN)
assert.throws(() => parse('999e999'))
assert.equal(parse('-0'), -0)
assert.equal(parse('179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368'), Number.MAX_VALUE)
assert.throws(() => parse('179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858367')) // one off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment