【问题标题】:Large numbers erroneously rounded in JavaScript在 JavaScript 中错误地四舍五入的大数
【发布时间】:2021-04-17 14:43:01
【问题描述】:

查看此代码:

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。

为什么会这样?

【问题讨论】:

  • 感谢所有快速有用的答案,我希望我可以将所有 3 标记为官方答案。

标签: javascript floating-point floating-accuracy ieee-754


【解决方案1】:

您超出了 JavaScript 的 number 类型的容量,有关详细信息,请参阅 §8.5 of the spec。这些 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 + 19007199254740992,但 9007199254740992 + 1 也是 9007199254740992 因为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 位的精度)。指数的使用方式很复杂 (described here),但用非常模糊的术语来说,如果我们在指数上加一,则有效数字的值加倍,因为指数用于2(再次提醒一下,这不是直接的,里面有聪明之处)。

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

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 符号位 / +−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−-−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 指数 // | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− +− 有效数 // | / | 0 10000110011 111111111111111111111111111111111111111111111111111 = 9007199254740991 (Number.MAX_SAFE_INTEGER)

那个指数值,10000110011,意味着每次我们在有效位上加一,表示的数字就会增加 1(整数 1,我们更早失去了表示小数的能力)。

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

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 符号位 / +−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−-−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 指数 // | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− +− 有效数 // | / | 0 10000110100 00000000000000000000000000000000000000000000000000000 = 9007199254740992 (Number.MAX_SAFE_INTEGER + 1)

好吧,没关系,因为无论如何9007199254740991 + 1 就是9007199254740992。但!我们不能代表9007199254740993。我们已经用完了比特。如果我们只给有效数字加 1,它就会给值加 2:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 符号位 / +−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−-−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− −−−−−−−−−−−−−−− 指数 // | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− +− 有效数 // | / | 0 10000110100 00000000000000000000000000000000000000000000000000001 = 9007199254740994 (Number.MAX_SAFE_INTEGER + 3)

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

最终,我们又用完了有效位,必须增加指数,所以我们最终只能表示 4 的倍数,然后是 8 的倍数,然后是 16 的倍数,以此类推。

【讨论】:

  • 我喜欢这个答案,因为它实际上告诉你如何解决问题。
  • 这是一个很棒的答案,正是我想要的。
【解决方案2】:

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

【讨论】:

  • 15位有效十进制数字“143412520769791”而不是“714341252076979”是我不明白的地方
  • 这个答案似乎有两个错误:1) 次要,前导7 缺少最后一个数字,2) 主要,输出是not四舍五入到 15 位-- 它也是 53 位尾数浮点数的最接近表示形式,大约需要 15.95 个十进制数字。 ...100 部分不如舍入稳定,例如...79135 errs 到 ...79100...79136 errs 到 ...79200,甚至这个 ...35/...36 限制也会任意漂移。 (迂腐模式:从某种意义上说,它四舍五入,因为它“四舍五入”到小数点后 15.95 位)
【解决方案3】:

不是这个json解析器造成的。只需尝试在 fbug 的控制台中输入 714341252076979033 即可。您将看到相同的 714341252076979100。

详情请参阅此博文: http://www.exploringbinary.com/print-precision-of-floating-point-integers-varies-too

【讨论】:

  • 感谢您链接到我的文章,但它只解释了问题的一半——内部舍入值的打印。即使 javascript 让您打印整个内容,它仍然是错误的——它将是最接近的可表示的双精度值,如下面的其他人所解释的那样。
【解决方案4】:

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

ceil(lb 714341252076979033) = 60

精确表示值的位。

最接近的可精确表示的数字是714341252076979072(将原始数字写成二进制,用0替换最后7位数字并向上取整,因为替换的最高数字是1)。

您将得到 714341252076979100 而不是这个数字,因为 ToString() 如 ECMA-262 所述,§9.8.1 使用 10 的幂并且在 53 位精度下所有这些数字都是相等的。

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      JavaScript 只能处理高达约 90 亿的精确整数(即 9 和 15 个零)。高于这个,你会得到垃圾。通过使用字符串来保存数字来解决这个问题。如果您需要对这些数字进行数学运算,请编写自己的函数或查看是否可以为它们找到库:我建议使用前者,因为我不喜欢我见过的库。为了帮助您入门,请参阅我的两个函数 another answer

      【讨论】:

        猜你喜欢
        • 2020-08-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-09-02
        相关资源
        最近更新 更多