Convert integers to binary strings
If you want to visualize the binary digits of a number in JavaScript, you need to decode the floating point format as you build your string.
The simplest, but least accurate method is to use the built-in Number.toString(2)
.
JavaScript
let n = 254;
let nStr = n.toString(2);
console.log(nStr);
// outputs "11111110"
There are two issues with that output.
- It doesn't pad the result correctly, giving the false impression that
254
is stored in memory using only 8 bits. - Negative numbers won't be in two's-complement format. Instead,
toString()
converts the number's absolute value to binary, then prepends a '-'.
(1) is relatively simple to handle with String.padStart()
. (2) can be solved with a quirk of the >>>
operator:
let n = -255;
let nStr = (n >>> 0).toString(2);
console.log(nStr);
// outputs "11111111111111111111111100000001"
">>>
" apparently forces a conversion to an unsigned integer, whose bits are treated as-is by toString()
.
That result is pretty decent, but it won't cover numbers closer to the Number.MAX_SAFE_INTEGER range. The output above was only 32 bits long, but JavaScript numbers actually offer 52 bits of precision.
To get an accurate representation of any integer in the range [Number.MIN_SAFE_INTEGER
, Number.MAX_SAFE_INTEGER
], you'll need to do more work to decode the floating point container format.
This SO answer has an excellent solution:
JavaScript
// IIFE to scope internal variables
var float64ToInt64Binary = (function () {
// create union
var flt64 = new Float64Array(1);
var uint16 = new Uint16Array(flt64.buffer);
// 2**53-1
var MAX_SAFE = 9007199254740991;
// 2**31
var MAX_INT32 = 2147483648;
function uint16ToBinary() {
var bin64 = "";
// generate padded binary string a word at a time
for (var word = 0; word < 4; word++) {
bin64 = uint16[word].toString(2).padStart(16, 0) + bin64;
}
return bin64;
}
return function float64ToInt64Binary(number) {
// NaN would pass through Math.abs(number) > MAX_SAFE
if (!(Math.abs(number) <= MAX_SAFE)) {
throw new RangeError("Absolute value must be less than 2**53");
}
var sign = number < 0 ? 1 : 0;
// shortcut using other answer for sufficiently small range
if (Math.abs(number) <= MAX_INT32) {
return (number >>> 0).toString(2).padStart(64, sign);
}
// little endian byte ordering
flt64[0] = number;
// subtract bias from exponent bits
var exponent = ((uint16[3] & 0x7ff0) >> 4) - 1023;
// encode implicit leading bit of mantissa
uint16[3] |= 0x10;
// clear exponent and sign bit
uint16[3] &= 0x1f;
// check sign bit
if (sign === 1) {
// apply two's complement
uint16[0] ^= 0xffff;
uint16[1] ^= 0xffff;
uint16[2] ^= 0xffff;
uint16[3] ^= 0xffff;
// propagate carry bit
for (var word = 0; word < 3 && uint16[word] === 0xffff; word++) {
// apply integer overflow
uint16[word] = 0;
}
// complete increment
uint16[word]++;
}
// only keep integer part of mantissa
var bin64 = uint16ToBinary().substr(11, Math.max(exponent, 0));
// sign-extend binary string
return bin64.padStart(64, sign);
};
})();
Credit to Patrick Roberts for the solution.
[It] creates a union between a 64-bit floating point number and an unsigned 16-bit integer array [...] then uses the union to gain bit access to the value and calculate the binary string based on the unbiased binary exponent and fraction bits.
Using float64ToInt64Binary
will give you a bitwise-correct integer in the full valid range. His solution relies heavily on knowledge of the floating point format. The Wikipedia entry is pretty dense, so I recommend "The Floating-Point Guide" if you want to learn more.
References: