Why does Number("x") == BigInt("x") ... only sometimes?

Viewed 1722

I thought that JavaScript's loose equality operator was being nice and letting me compare Numbers with BigInts:

42 == 42n  // true!

So I tried a number bigger than Number.MAX_SAFE_INTEGER. I figured that since the number gets rounded up automatically, I might have to round up the BigInt as well for them to be considered equal:

9999999999999999 == 10000000000000000   // true - rounded to float64

9999999999999999  == 9999999999999999n  // false - makes sense!
10000000000000000 == 9999999999999999n  // false - makes sense!
9999999999999999  == 10000000000000000n // true  - makes sense!

Great, makes sense — so then I tried another big number that gets rounded up:

18446744073709551616 == 18446744073709552000   // true - rounded to float64

18446744073709551616 == 18446744073709551616n  // true?!
18446744073709552000 == 18446744073709551616n  // true?!
18446744073709551616 == 18446744073709552000n  // false?!

I observed the same results in Chrome, Safari, and Node.js.

Why isn't this behavior consistent? Is it because the numbers are compared as mathematical values, and what does that mean?

bigint equality table

2 Answers

The inconsistency is because the Number::toString abstract operation is underspecified.

Your question boils down to BigInt(String(x))BigInt(x), which might assumed to be an equality for integer numbers x but is in fact not.

In your particular case, for x=18446744073709551616 or x=18446744073709552000 (or anything in between, and even a bit around), the string representation of the number yields '18446744073709552000' whereas the exact mathematical value is 18446744073709551616. (We know this because the NumberToBigInt operation is exact - it gets you the mathematical value of the number, or an error if it's not an integer).

We also find the following note on Number.prototype.toFixed:

The output of toFixed may be more precise than toString for some values because toString only prints enough significant digits to distinguish the number from adjacent Number values. For example,

(1000000000000000128).toString() returns "1000000000000000100", while
(1000000000000000128).toFixed(0) returns "1000000000000000128".


To answer the titular question

Why does Number(“x”) == BigInt(“x”) … only sometimes?

It's because the limited precision of floating point Number values. There are multiple numeric literals that are parsed to exactly the same number. Similar to your first example, let's take the bigint 20000000000000000n. There is a floating point number with the same mathematical value, specifically

   +1 × 0b10001110000110111100100110111111000001 × 20b10001
= 1 × 152587890625 × 217
= 20000000000000000

There are multiple integer number literals that evaluate to this Number value: 19999999999999998, 19999999999999999, 20000000000000000, 20000000000000001, and 20000000000000002. (Notice it's not always rounding up). This is also what happens when you take these as strings and use unary + or Number on them.

But if you take the respective bigint literals with the same textual representation, they will evaluate to 5 different BigInt values. Only one of which will compare equal (with ==) to, i.e. have the same mathematical value as, the Number value.

This is consistent: there's always exactly one, and it's the one that can be represented precisely as a floating point number.

Why isn't this behavior consistent?

Your confusion comes from the string representation of the Number value 18446744073709551616. When printing the number literal 18446744073709551616, or also +'18446744073709551616', you got 18446744073709552000 (because the console uses String()/.toString() internally), which made you assume that 18446744073709552000 was its mathematical value, and that 18446744073709552000n should compare equal to it. But it's not :-/

console.log(18446744073709551616..toString());
console.log(18446744073709551616..toFixed());
console.log(18446744073709552000..toString());
console.log(18446744073709552000..toFixed());

Which to believe?

My guess is that this is due to the exact implementation of mathematical value not being specified (or at least I couldn't find it).

The abstract equality comparison is specified https://262.ecma-international.org/11.0/#sec-abstract-equality-comparison

If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then

If x or y are any of NaN, +∞, or -∞, return false.

If the mathematical value of x is equal to the mathematical value of y, return true; otherwise return false.

So a BigInt should have a mathematical value of some integer, and a Number should have a mathematical value of some kind of scientific notation number, and so the comparisons should be intuitive, but exactly how your JS engine implements mathematical value is not following the spec in spirit.

Related