How to convert a BigInt, to two's complement binary in JavaScript?

Viewed 94

With "normal" numbers(32bit range), i'm using zero fill right shift operator to convert to binary, which works both with positive and negative numbers(results in the two's complement binary):

const numberToConvert = -100
(numberToConvert  >>> 0).toString(2);
//Result is correct, in two's complement: '11111111111111111111111110011100'

But how can this be done with a negative BigInt? If i do:

(-1000000000000000000n >>> 0).toString(2)

I get an error "Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions"

So then i try to use 0 as a bigint:

(-1000000000000000000n >>> 0n).toString(2)

I get the following error: Uncaught TypeError: BigInts have no unsigned right shift, use >> instead

Doing so, results in the non two's complement binary, with "-" appended to it:

(-1000000000000000000n >> 0n).toString(2)
//Result is:'-110111100000101101101011001110100111011001000000000000000000'

How can I get the two's complement binary, of a negative bigint?

2 Answers

Two's complement only makes sense with fixed bit lengths. Numbers are converted to 32-bit integers (this is an old convention from back when javascript was messier). BigInt doesn't have that kind of conversion as the length is considered arbitrary. So, in order to use two's complement with BigInt, you'll need to figure out what length you want to use then convert it. Conversion to two's complement is described many places including Wikipedia.

Here, we use the LSB to MSB method since it's pretty easy to implement as string processing in javascript:

const toTwosComplement = (n, len) => {
  // `n` must be an integer
  // `len` must be a positive integer greater than bit-length of `n`

  n = BigInt(n);

  len = Number(len);
  if(!Number.isInteger(len)) throw '`len` must be an integer';
  if(len <= 0) throw '`len` must be greater than zero';
  
  // If non-negative, a straight conversion works
  if(n >= 0){
    n = n.toString(2)
    if(n.length >= len) throw 'out of range';
    return n.padStart(len, '0');
  }
  
  n = (-n).toString(2); // make positive and convert to bit string
  
  if(!(n.length < len || n === '1'.padEnd(len, '0'))) throw 'out of range';
  
  // Start at the LSB and work up. Copy bits up to and including the
  // first 1 bit then invert the remaining
  let invert = false;
  return n.split('').reverse().map(bit => {
    if(invert) return bit === '0' ? '1' : '0';
    if(bit === '0') return bit;
    invert = true;
    return bit;
  }).reverse().join('').padStart(len, '1');
};

console.log(toTwosComplement( 1000000000000000000n, 64));
console.log(toTwosComplement(-1000000000000000000n, 64));
console.log(toTwosComplement(-1, 64));
console.log(toTwosComplement(2n**63n-1n, 64));
console.log(toTwosComplement(-(2n**63n), 64));
div.as-console-wrapper{max-height:none;height:100%;}

The bitwise operators are for 32-bit integers anyway, and why it doesn't work with BigInt, as quoted in JavaScript Definitive Guide, 7th Ed, David Flanagan, O'Reilly, p. 78:

Shift right with zero fill (>>>): This is the only one of the JavaScript bitwise operators that cannot be used with BigInt values. BigInt does not represent negative numbers by setting the high bit the way that 32-bit integers do, and this operator only makes sense for that particular two’s complement representation.

Also note that it looks like it is giving you two's complement, but in fact, the negative number is converted to 32-bit unsigned integer, and then printed as binary, giving you the impression that it is two's complement:

console.log(-100 >>> 0);     // => 4294967196

The two's complement has this property:

  1. You have a number, say 123, which is 01111011 in 8 bit binary, and you want the negative number of that, which is -123.

  2. Two complement says: the answer you want, just treat it as a positive number, and add it with the original number 123, and you will just get all 0's with the overflow of the 8 bit number.

  3. As an example, treating everything as positive, 123 + theAnswerYouWant is 01111011 + 10000101, which is exactly 00000000 with an overflow, which is 100000000 (note the extra 1 in front). In other words, you want 256 - 123, which is 133 and if you render 133 as 8 bit, that's the answer you want.

  4. As a result, you can use 28 to subtract the orignal number, and treat it as a positive number and display it, using .toString(2), which you already have.

  5. The following is for 64 bits:

function getBinary(a, nBits) {
  [a, nBits] = [BigInt(a), BigInt(nBits)];

  if ((a > 0 && a >= 2n ** (nBits - 1n)) || (a < 0 && -a > 2n ** (nBits - 1n))) {
    throw new RangeError("overflow error");
  }

  return a >= 0
    ? a.toString(2).padStart(Number(nBits), "0")
    : (2n ** nBits + a).toString(2);
}

console.log(getBinary(1000000000000000000n, 64));
console.log(getBinary(-1000000000000000000n, 64));
console.log(getBinary(-1, 64));
console.log(getBinary(-2, 64));
console.log(getBinary(-3, 64));
console.log(getBinary(-4, 64n));   // trying the nBits as a BigInt as a test
console.log(getBinary(2n ** 63n - 1n, 64));
console.log(getBinary(-(2n ** 63n), 64));

// console.log(getBinary(2n ** 63n, 64));           // throw Error
// console.log(getBinary(-(2n ** 63n) - 1n, 64));   // throw Error

Note that you don't have to pad it when a is negative, because for example, if it is 8 bit, the number being displayed is any where from 11111111 to 10000000 and it is always 8 bits.

Some more details:

  1. You may already know ones' complement is just simply flipping the bits (from 0 to 1, and 1 to 0). Another way to think of it is, you add the two numbers together and it will becomes all 1s.
  2. The usual way two's complement is described, is to flip the bits, and add 1 to it. You see, if you start with 11111111 and subtract 01111011 (which is 123 decimal), you get 10000100 and it is exactly the same as flipping the bit. (actually this follows from above: adding them get all 1s, so using all 1s to subtract one of them get the other one.
  3. Well, so if you start with 11111111 and subtract that number, and then add 1, isn't it the same as using 11111111, add 1, and subtract that number? Well, 11111111 plus 1 is 100000000 (note the extra 1 in front) -- that's exactly starting with 2n where n is the n-bit integer, and then subtract that number. So you see why the property at the beginning of this post is true.
  4. In fact, two's complement is designed with such purpose: if we want to find out 2 - 1, to make the computer calculate that, we only need to consider this "two's complement" as positive numbers and add them together using the processor's "add circuitry": 00000010 plus 11111111. We get 00000001 but have a carry (the overflow). If we handle the overflow correctly by discarding it, we get the answer: 1. If we use ones' complement instead, we can't use the same addition circuitry to carry out 00000010 + 11111110 to get a 1 because the result is 00000000 which is 0
  5. Another way to think about (4) is, if you have a car's odometer, and it says 000002 miles so far, how do you subtract 1 from it? Well, if you represent -1 as 9999999, then you just add 999999 to the 2, and get 1000001 but the leftmost 1 does not show on the odometer, and now the odometer will become 000001. In decimal, representing -1 as 999999 is 10's complement. In binary, representing -1 as 11111111 is called two's complement.
Related