Skip to content

Instantly share code, notes, and snippets.

@qntm
Last active December 10, 2021 19:32
Show Gist options
  • Save qntm/5c3d90cb3c5673f8042651dca753991d to your computer and use it in GitHub Desktop.
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')
}
@qntm
Copy link
Author

qntm commented Dec 9, 2021

Examples:

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

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

assert.strictEqual(parse('Infinity'), Infinity)
assert.strictEqual(parse('-Infinity'), -Infinity)
assert.strictEqual(parse('NaN'), NaN)
assert.throws(() => parse('999e999'))
assert.strictEqual(parse('-0'), -0)
assert.strictEqual(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