JavaScript 中的大数字被错误地舍入

发布于 2024-08-04 02:10:55 字数 541 浏览 2 评论 0原文

看这段代码:

var jsonString = '{"id":714341252076979033,"type":"FUZZY"}';
var jsonParsed = JSON.parse(jsonString);
console.log(jsonString, jsonParsed);

当我在 Firefox 3.5 中看到控制台时,jsonParsed 的值是四舍五入的数字:

Object id=714341252076979100 type=FUZZY

尝试了不同的值,相同的结果(四舍五入的数字)。

我也不明白它的舍入规则。 714341252076979136 四舍五入为 714341252076979200,而 714341252076979135 四舍五入为 714341252076979100。

为什么会发生这种情况?

See this code:

var jsonString = '{"id":714341252076979033,"type":"FUZZY"}';
var jsonParsed = JSON.parse(jsonString);
console.log(jsonString, jsonParsed);

When I see my console in Firefox 3.5, the value of jsonParsed is the number rounded:

Object id=714341252076979100 type=FUZZY

Tried different values, the same outcome (number rounded).

I also don't get its rounding rules. 714341252076979136 is rounded to 714341252076979200, whereas 714341252076979135 is rounded to 714341252076979100.

Why is this happening?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

您的好友蓝忘机已上羡 2024-08-11 02:10:55

您溢出了 JavaScript number 类型的容量,请参阅 规范的§8.5关于 IEEE-754 double 的维基百科页面- 精度二进制浮点数了解详细信息。这些 ID 需要是字符串。

IEEE-754 双精度浮点(JavaScript 使用的数字类型)无法精确表示所有数字(当然)。众所周知,0.1 + 0.2 === 0.3 是错误的。这会影响整数,就像影响小数一样;一旦您超过 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER),它就会开始。

除了 Number.MAX_SAFE_INTEGER + 1 (9007199254740992) 之外,IEEE-754 浮点格式无法再表示每个连续的整数。 9007199254740991 + 1 + 1 是 9007199254740992,但 9007199254740992 + 1 也是 9007199254740992 因为 < code>9007199254740993 无法以该格式表示。下一个可能是9007199254740994。那么 9007199254740995 不能,但 9007199254740996 可以。

原因是我们已经用完了位,所以我们不再有 1 位;最低位现在代表 2 的倍数。最终,如果我们继续下去,我们会失去该位,并且只能以 4 的倍数工作。依此类推。

您的值远远高于该阈值,因此它们会四舍五入到最接近的可表示值。

从 ES2020 开始,您可以使用 BigInt用于任意大的整数,但它们没有 JSON 表示形式。您可以使用字符串和 reviver 函数:

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.)


如果您对这些位感到好奇,会发生以下情况:IEEE-754 二进制双精度浮点数有一个符号位,即 11 位指数(它定义了数字的总体范围,作为 2 的幂 [因为这是二进制格式])和 52 位有效数(但该格式非常聪明,它从这 52 位中获得了 53 位精度)。指数的使用方式很复杂(此处描述),但用非常的术语来说,如果我们给指数加一,有效数的值就会加倍,因为指数用于 2 的幂(再次强调,它不是直接的,里面有聪明之处)。

让我们看看值 9007199254740991(又名 Number.MAX_SAFE_INTEGER):

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

该指数值 10000110011 意味着每次我们加一有效数,表示的数字增加 1(整数 1,我们很早就失去了表示小数的能力)。

但现在该有效数已满。要超过该数字,我们必须增加指数,这意味着如果我们在有效数上加 1,则表示的数字的值会增加 2,而不是 1(因为指数应用于 2,即该数字的底数)二进制浮点数):

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

好吧,没关系,因为 9007199254740991 + 1 无论如何都是 9007199254740992 。但!我们无法代表 9007199254740993。我们的位已经用完了。如果我们只在尾数上加 1,它就会在值上加 2:

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

当我们增加值时,格式就不能再表示奇数了,指数太大了。

最终,我们再次用完了有效位数,并且必须增加指数,因此我们最终只能表示 4 的倍数。然后是 8 的倍数。然后是 16 的倍数。依此类推。

You're overflowing the capacity of JavaScript's number type, see §8.5 of the spec and the Wikipedia page on IEEE-754 double-precision binary floating point 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.

岛徒 2024-08-11 02:10:55

您在这里看到的实际上是两次舍入的效果。 ECMAScript 中的数字在内部表示为双精度浮点数。当id设置为714341252076979033(十六进制的0x9e9d9958274c359)时,它实际上被分配了最接近的可表示的双精度值,即7143412520769790720x9e9d9958274c380)。当您打印该值时,它会四舍五入到 15 位有效十进制数字,即 14341252076979100

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.

梦里人 2024-08-11 02:10:55

这个 JSON 解析器不会导致它。只需尝试在 fbug 的控制台中输入 714341252076979033 即可。您会看到相同的 714341252076979100。有关详细信息,请参阅此博客文章:floating-点

This JSON parser does not cause it. Just try to enter 714341252076979033 to fbug's console. You'll see the same 714341252076979100. See this blog post for details: floating-point

酸甜透明夹心 2024-08-11 02:10:55

JavaScript 使用双精度浮点值,即总精度为 53 位,但您需要

ceil(lb 714341252076979033) = 60

位来精确表示该值。

最接近的完全可表示的数字是 714341252076979072 (以二进制形式写入原始数字,用 0 替换最后 7 位数字并向上舍入,因为最高替换数字是 1< /代码>)。

您将得到 714341252076979100 而不是这个数字,因为 ECMA-262、§9.8.1 中描述的 ToString() 使用 10 的幂并且以 53 位精度工作所有这些数量相等。

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.

花开柳相依 2024-08-11 02:10:55

问题是你的数字需要比 JavaScript 更高的精度。

您可以将号码作为字符串发送吗?分成两部分?

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?

我偏爱纯白色 2024-08-11 02:10:55

JavaScript 只能处理最多大约 90 亿个整数(即 9 个数字和 15 个零)。高于这个值,你就会得到垃圾。通过使用字符串来保存数字来解决这个问题。如果您需要对这些数字进行数学运算,请编写自己的函数或看看是否可以找到它们的库:我建议前者,因为我不喜欢我见过的库。为了帮助您开始,请参阅我的两个函数 另一个答案< /a>.

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文