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?
The easiest way to determine if you're working with a BigInt is to look for the letter n in your code following a numeric value. The letter n means the value is a BigInt ( 37n ), not a number ( 37 ).
Syntax. To differentiate a BigInt from a Number you have to append the letter n to the end of the number. For example, 156 is a Number, 156n is a BigInt.
BigInt is a special numeric type that provides support for integers of arbitrary length. A bigint is created by appending n to the end of an integer literal or by calling the function BigInt that creates bigints from strings, numbers etc.
BigInt is a new data type intended for use when integer values are larger than the range supported by the Number data type. This data type allows us to safely perform arithmetic operations on large integers, represent high-resolution timestamps, use large integer IDs, and more without the need to use a library.
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 thantoString
for some values becausetoString
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With