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.

  1. It doesn't pad the result correctly, giving the false impression that 254 is stored in memory using only 8 bits.
  2. 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:

  1. https://modernweb.com/what-every-javascript-developer-should-know-about-floating-points/
  2. http://floating-point-gui.de/formats/fp/
  3. http://fabiensanglard.net/floating_point_visually_explained/index.php