peterolson / BigInteger.js

An arbitrary length integer library for Javascript
The Unlicense
1.12k stars 187 forks source link

Loses information when parsing large JavaScript numbers #96

Closed qntm closed 7 years ago

qntm commented 7 years ago
var x = 999999999999999000034;
console.log(x); // 999999999999999000000
console.log(x % 10000000); // 8951424

Throughout this example code, the value of x is precisely 999999999999998951424, since this is the nearest available float to the number which was asked for by the source code. We see at the second line that, when printing x out, JavaScript only supplies the bare minumum number of decimal places required to uniquely specify a float with the same value, however, this is a big mislead. At the third line, we demonstrate that, for arithmetical purposes, which is what we care about, x is 999999999999998951424.

I think big-integer should likewise favour the arithmetical meaning of a number, but:

var y = bigInt(x);
console.log(y.toString()); // '999999999999999000000'
console.log(y.mod(10000000).toString()); // '9000000'

It looks as if we favour the lossy JavaScript string representation over the actual number which x represents.

[some stuff removed here]

The resolution would be to inspect x more carefully to determine its IEEE754 radix and mantissa, then use these to construct a big-integer object from it.

Of course, this would be a change from existing behaviour, which possibly is already expected by users...

peterolson commented 7 years ago

JavaScript numbers are allowed as inputs to bigInt primarily for convenience when using small numbers. I can't think of any reasonable use case for a user wanting to pass in a number too large to be represented by Number.

In JavaScript the numbers 999999999999999000000, 999999999999999000034, and 999999999999998951424 are indistinguishable. For example 999999999999998951424 === 999999999999999000034 returns true. Once they're passed into bigInteger it's impossible to recover the original form, so any one of these (among thousands of other options) is an equally valid interpretation.

In general, this library tries to maintain parity with the behavior of JavaScript to avoid unexpected inconsistencies, so matching the behavior of toString seems to make the most sense to me.

It would be a surprise for users if bigInt(x).toString() did not match x.toString().

In general I believe that if Number.isInteger(x) is true, bigInt(x).toJSNumber() === x should also be true, since the big integers are a superset of the little ones.

Can you give an example where this does not hold? Using the example you gave, bigInt(999999999999999000034).toJSNumber() === 999999999999999000034 evaluates to true.

qntm commented 7 years ago

Ah, so bringing up that last assertion about things which should be true was a mistake on my part. It doesn't relate to the problem I'm describing, I shouldn't have brought it up. I have removed it from what I originally wrote. Sorry!

Anyway, my use case was that I wanted to know what the exact value of Number.MAX_VALUE was, and maybe do some arithmetic with it, so I did bigInt(Number.MAX_VALUE).toString() and what I got was

'179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'

The actual value of Number.MAX_VALUE is 21024 - 2971:

179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368

This discrepancy caused some later calculations to fail. It took me a while to trace the reason why.

I discovered this because I am putting together an API which accepts both JavaScript numbers and bigIntegers. When a user supplies me with a JavaScript number, I convert it to a bigInteger, under the assumption that the value that the user has provided is exact. However, big-integer introduces an error at this stage.

When you say "In JavaScript the numbers 999999999999999000000, 999999999999999000034, and 999999999999998951424 are indistinguishable", I would argue that JavaScript simply does not have the numbers 999999999999999000000 or 999999999999999000034. It only has 999999999999998951424. Arithmetic using 999999999999998951424 should behave as such, including big-integer arithmetic.

peterolson commented 7 years ago

I understand what you're getting at, but I'm hesitant to make this change for a number of reasons.

  1. As I said before, it breaks the parity between bigInt(x).toString() and x.toString().
  2. When you do bigInt(999999999999999000034).toString() and see the result "999999999999999000000", it's straightforward to understand what's going on behind the scenes and realize that the number passed in was truncated because it's too large to be represented accurately with a native JavaScript number. After seeing bigInt(999999999999999000000).toString() result in "999999999999998951424", most people would think "wtf is going on?!".
  3. I haven't thought very hard yet about how you would implement this, but I suspect that this would be more computationally expensive than the current toString method which typically just redirects to the native JavaScript toString. Performance considerations are relatively important for toString since it is one of the most commonly called methods.
qntm commented 7 years ago

This would be a change at bigInt construction time rather than at stringification time. Does that change the performance considerations?