71

Anyone knows why javascript Number.toString function does not represents negative numbers correctly?

//If you try
(-3).toString(2); //shows "-11"
// but if you fake a bit shift operation it works as expected
(-3 >>> 0).toString(2); // print "11111111111111111111111111111101"

I am really curious why it doesn't work properly or what is the reason it works this way? I've searched it but didn't find anything that helps.

fernandosavio
  • 9,849
  • 4
  • 24
  • 34

6 Answers6

34

Short answer:

  1. The toString() function takes the decimal, converts it to binary and adds a "-" sign.

  2. A zero fill right shift converts it's operands to signed 32-bit integers in two complements format.

A more detailed answer:

Question 1:

//If you try
(-3).toString(2); //show "-11"

It's in the function .toString(). When you output a number via .toString():

Syntax

numObj.toString([radix])

If the numObj is negative, the sign is preserved. This is the case even if the radix is 2; the string returned is the positive binary representation of the numObj preceded by a - sign, not the two's complement of the numObj.

It takes the decimal, converts it to binary and adds a "-" sign.

  1. Base 10 "3" converted to base 2 is "11"
  2. Add a sign gives us "-11"

Question 2:

// but if you fake a bit shift operation it works as expected
        (-3 >>> 0).toString(2); // print "11111111111111111111111111111101"

A zero fill right shift converts it's operands to signed 32-bit integers. The result of that operation is always an unsigned 32-bit integer.

The operands of all bitwise operators are converted to signed 32-bit integers in two's complement format.

Daan
  • 7,685
  • 5
  • 43
  • 52
  • 1
    This is the only one correct answer, had implementation of this operator been revised after 2013 ... ? – Carr Mar 01 '18 at 17:24
  • The result of the *unsigned* right shift operator is an *unsigned* 32-bit integer. See https://tc39.es/ecma262/#sec-unsigned-right-shift-operator. – MikeM Jun 20 '19 at 17:59
30

-3 >>> 0 (right logical shift) coerces its arguments to unsigned integers, which is why you get the 32-bit two's complement representation of -3.

http://en.wikipedia.org/wiki/Two%27s_complement

http://en.wikipedia.org/wiki/Logical_shift

Steve Wang
  • 1,814
  • 1
  • 16
  • 12
  • Why an unsigned? Unsigned integers in C++ aren't negative and never use two's complement. It's signed integers that can be negative and it's binary value is represented by two's complement. – Adam Dreaver Jul 29 '13 at 21:34
  • This question was about Javascript, not C++. Also, since you're coercing a negative integer to an unsigned integer, the only sane result (aside from maybe telling you "don't do that") is to return the two's complement result (so basic arithmetic at least works the same). – Steve Wang Aug 12 '13 at 19:33
  • 2
    @SteveWang It says [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Unsigned_right_shift) that "the operands of all bitwise operators are converted to _signed_ 32-bit integers in two's complement format", not _unsigned_. – nglee Jan 31 '18 at 08:44
  • @nglee, that is probably because Steve does not blindly believe what is on MDN. (+1) – trincot Mar 28 '19 at 15:13
  • 2
    At the time this was posted, the current Javascript spec did in fact dictate that both arguments be cast to unsigned 32-bit integers (steps 5-6 of https://262.ecma-international.org/5.1/#sec-11.7.3). Since then, it's been revised to keep lnum as a signed integer, then implicitly bit-cast it to an unsigned integer at the end. https://262.ecma-international.org/11.0/#sec-numeric-types-number-unsignedRightShift – Steve Wang Jun 02 '21 at 16:44
23
var binary = (-3 >>> 0).toString(2); // coerced to uint32

console.log(binary);

console.log(parseInt(binary, 2) >> 0); // to int32

on jsfiddle

output is

11111111111111111111111111111101
-3 
Xotic750
  • 22,914
  • 8
  • 57
  • 79
6

Just to summarize a few points here, if the other answers are a little confusing:

  • what we want to obtain is the string representation of a negative number in binary representation; this means the string should show a signed binary number (using 2's complement)
  • the expression (-3 >>> 0).toString(2), let's call it A, does the job; but we want to know why and how it works
  • had we used var num = -3; num.toString(-3) we would have gotten -11, which is simply the unsigned binary representation of the number 3 with a negative sign in front, which is not what we want
  • expression A works like this:

1) (-3 >>> 0)

The >>> operation takes the left operand (-3), which is a signed integer, and simply shifts the bits 0 positions to the left (so the bits are unchanged), and the unsigned number corresponding to these unchanged bits.

The bit sequence of the signed number -3 is the same bit sequence as the unsigned number 4294967293, which is what node gives us if we simply type -3 >>> 0 into the REPL.

2) (-3 >>> 0).toString

Now, if we call toString on this unsigned number, we will just get the string representation of the bits of the number, which is the same sequence of bits as -3.

What we effectively did was say "hey toString, you have normal behavior when I tell you to print out the bits of an unsigned integer, so since I want to print out a signed integer, I'll just convert it to an unsigned integer, and you print the bits out for me."

evianpring
  • 3,316
  • 1
  • 25
  • 54
5

.toString() is designed to return the sign of the number in the string representation. See EcmaScript 2015, section 7.1.12.1:

  1. If m is less than zero, return the String concatenation of the String "-" and ToString(−m).

This rule is no different for when a radix is passed as argument, as can be concluded from section 20.1.3.6:

  1. Return the String representation of this Number value using the radix specified by radixNumber. [...] the algorithm should be a generalization of that specified in 7.1.12.1.

Once that is understood, the surprising thing is more as to why it does not do the same with -3 >>> 0.

But that behaviour has actually nothing to do with .toString(2), as the value is already different before calling it:

console.log (-3 >>> 0); // 4294967293

It is the consequence of how the >>> operator behaves.

It does not help either that (at the time of writing) the information on mdn is not entirely correct. It says:

The operands of all bitwise operators are converted to signed 32-bit integers in two's complement format.

But this is not true for all bitwise operators. The >>> operator is an exception to the rule. This is clear from the evaluation process specified in EcmaScript 2015, section 12.5.8.1:

  1. Let lnum be ToUint32(lval).

The ToUint32 operation has a step where the operand is mapped into the unsigned 32 bit range:

  1. Let int32bit be int modulo 232.

When you apply the above mentioned modulo operation (not to be confused with JavaScript's % operator) to the example value of -3, you get indeed 4294967293.

As -3 and 4294967293 are evidently not the same number, it is no surprise that (-3).toString(2) is not the same as (4294967293).toString(2).

trincot
  • 317,000
  • 35
  • 244
  • 286
1

Daan's answer explains it well.

toString(2) does not really convert the number to two's complement, instead it just do simple translation of the number to its positive binary form, while preserve the sign of it.

Example

Assume the given input is -15,
1. negative sign will be preserved
2. `15` in binary is 1111, therefore (-15).toString(2) gives output
-1111 (this is not in 2's complement!)

We know that in 2's complement of -15 in 32 bits is
11111111 11111111 11111111 11110001

Therefore in order to get the binary form of (-15), we can actually convert it to unsigned 32 bits integer using the unsigned right shift >>>, before passing it to toString(2) to print out the binary form. This is the reason we do (-15 >>> 0).toString(2) which will give us 11111111111111111111111111110001, the correct binary representation of -15 in 2's complement.

ken
  • 13,869
  • 6
  • 42
  • 36