Large numbers erroneously rounded in JavaScript

后端 未结 6 1120
夕颜
夕颜 2020-11-21 23:58

See this code:



        
相关标签:
6条回答
  • 2020-11-22 00:26

    JavaScript uses double precision floating point values, ie a total precision of 53 bits, but you need

    ceil(lb 714341252076979033) = 60
    

    bits to exactly represent the value.

    The nearest exactly representable number is 714341252076979072 (write the original number in binary, replace the last 7 digits with 0 and round up because the highest replaced digit was 1).

    You'll get 714341252076979100 instead of this number because ToString() as described by ECMA-262, §9.8.1 works with powers of ten and in 53 bit precision all these numbers are equal.

    0 讨论(0)
  • 2020-11-22 00:27

    It is not caused by this json parser. Just try to enter 714341252076979033 to fbug's console. You'll see the same 714341252076979100.

    See this blog post for details: http://www.exploringbinary.com/print-precision-of-floating-point-integers-varies-too

    0 讨论(0)
  • 2020-11-22 00:31

    What you're seeing here is actually the effect of two roundings. Numbers in ECMAScript are internally represented double-precision floating-point. When id is set to 714341252076979033 (0x9e9d9958274c359 in hex), it actually is assigned the nearest representable double-precision value, which is 714341252076979072 (0x9e9d9958274c380). When you print out the value, it is being rounded to 15 significant decimal digits, which gives 14341252076979100.

    0 讨论(0)
  • 2020-11-22 00:33

    The problem is that your number requires a greater precision than JavaScript has.

    Can you send the number as a string? Separated in two parts?

    0 讨论(0)
  • 2020-11-22 00:37

    You're overflowing the capacity of JavaScript's number type, see §8.5 of the spec for details. Those IDs will need to be strings.

    IEEE-754 double-precision floating point (the kind of number JavaScript uses) can't precisely represent all numbers (of course). Famously, 0.1 + 0.2 == 0.3 is false. That can affect whole numbers just like it affects fractional numbers; it starts once you get above 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER).

    Beyond Number.MAX_SAFE_INTEGER + 1 (9007199254740992), the IEEE-754 floating-point format can no longer represent every consecutive integer. 9007199254740991 + 1 is 9007199254740992, but 9007199254740992 + 1 is also 9007199254740992 because 9007199254740993 cannot be represented in the format. The next that can be is 9007199254740994. Then 9007199254740995 can't be, but 9007199254740996 can.

    The reason is we've run out of bits, so we no longer have a 1s bit; the lowest-order bit now represents multiples of 2. Eventually, if we keep going, we lose that bit and only work in multiples of 4. And so on.

    Your values are well above that threshold, and so they get rounded to the nearest representable value.

    As of ES2020, you can use BigInt for integers that are arbitrarily large, but there is no JSON representation for them. You could use strings and a reviver function:

    const jsonString = '{"id":"714341252076979033","type":"FUZZY"}';
    // Note it's a string −−−−^−−−−−−−−−−−−−−−−−−^
    
    const obj = JSON.parse(jsonString, (key, value) => {
        if (key === "id" && typeof value === "string" && value.match(/^\d+$/)) {
            return BigInt(value);
        }
        return value;
    });
    
    console.log(obj);
    (Look in the real console, the snippets console doesn't understand BigInt.)


    If you're curious about the bits, here's what happens: An IEEE-754 binary double-precision floating-point number has a sign bit, 11 bits of exponent (which defines the overall scale of the number, as a power of 2 [because this is a binary format]), and 52 bits of significand (but the format is so clever it gets 53 bits of precision out of those 52 bits). How the exponent is used is complicated (described here), but in very vague terms, if we add one to the exponent, the value of the significand is doubled, since the exponent is used for powers of 2 (again, caveat there, it's not direct, there's cleverness in there).

    So let's look at the value 9007199254740991 (aka, Number.MAX_SAFE_INTEGER):

       +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
      / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
     / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
    / /         | /                                                  |
    0 10000110011 1111111111111111111111111111111111111111111111111111
                    = 9007199254740991 (Number.MAX_SAFE_INTEGER)
    

    That exponent value, 10000110011, means that every time we add one to the significand, the number represented goes up by 1 (the whole number 1, we lost the ability to represent fractional numbers much earlier).

    But now that significand is full. To go past that number, we have to increase the exponent, which means that if we add one to the significand, the value of the number represented goes up by 2, not 1 (because the exponent is applied to 2, the base of this binary floating point number):

       +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
      / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
     / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
    / /         | /                                                  |
    0 10000110100 0000000000000000000000000000000000000000000000000000
                    = 9007199254740992 (Number.MAX_SAFE_INTEGER + 1)
    

    Well, that's okay, because 9007199254740991 + 1 is 9007199254740992 anyway. But! We can't represent 9007199254740993. We've run out of bits. If we add just 1 to the significand, it adds 2 to the value:

       +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− sign bit
      / +−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− exponent
     / /        |  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+− significand
    / /         | /                                                  |
    0 10000110100 0000000000000000000000000000000000000000000000000001
                    = 9007199254740994 (Number.MAX_SAFE_INTEGER + 3)
    

    The format just cannot represent odd numbers anymore as we increase the value, the exponent is too big.

    Eventually, we run out of significand bits again and have to increase the exponent, so we end up only being able to represent multiples of 4. Then multiples of 8. Then multiples of 16. And so on.

    0 讨论(0)
  • 2020-11-22 00:42

    JavaScript can only handle exact whole numbers up to about 9000 million million (that's 9 with 15 zeros). Higher than that and you get garbage. Work around this by using strings to hold the numbers. If you need to do math with these numbers, write your own functions or see if you can find a library for them: I suggest the former as I don't like the libraries I've seen. To get you started, see two of my functions at another answer.

    0 讨论(0)
提交回复
热议问题