在 JavaScript 中错误地四舍五入的大数

IT技术 javascript floating-point floating-accuracy ieee-754
2021-01-15 06:12:37

看到这个代码:

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。

为什么会这样?

6个回答

您正在溢出 JavaScriptnumber类型的容量,有关详细信息,请参阅规范的第 8.5 节这些 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 位精度)。指数的使用方式很复杂(这里描述),但用非常模糊的术语来说,如果我们给指数加 1,有效数的值就会翻倍,因为指数用于 2 的幂(再次提醒,它是不是直接的,里面有聪明)。

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

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

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

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

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

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

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

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

最终,我们再次用完有效数位,不得不增加指数,所以我们最终只能表示 4 的倍数。然后是 8 的倍数,然后是 16 的倍数。等等。

这是一个绝妙的答案,正是我正在寻找的。
2021-03-17 06:12:37
我喜欢这个答案,因为它实际上告诉您如何解决问题。
2021-04-06 06:12:37

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

2021-03-18 06:12:37
这个答案似乎有两个错误:1)次要,7最后一个数字缺少前导,2)主要,输出没有四舍五入到 15 位——它也是 53 位尾数浮点数的最接近表示,它需要大约 15.95 位十进制数字。...100部分不像舍入那样稳定,例如...79135errs into...79100...79136errs into ...79200,甚至这个...35/...36限制也会随意漂移。(迂腐模式:从某种意义上说,它四舍五入,因为它“四舍五入”到小数点后 15.95 位)
2021-03-28 06:12:37
我不明白 15 个有效十进制数字“143412520769791”而不是“714341252076979”如何
2021-04-03 06:12:37

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

有关详细信息,请参阅此博客文章:http : //www.exploringbinary.com/print-precision-of-floating-point-integers-varies-too

感谢您链接到我的文章,但它只解释了一半的问题——内部舍入值的打印。即使 javascript 让您打印整个内容,它仍然是错误的——它将是最接近的可表示的双精度值,如下面的其他人所述。
2021-03-20 06:12:37

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

ceil(lb 714341252076979033) = 60

位来精确表示值。

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

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

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

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