JS中如何便捷地修复精度误差?

JS中如何便捷地修复精度误差?小伙伴经常会遇到类似0.1+0.2===0.3返回false的问题,这种问题非常隐晦。有时候又很明显,比如在价格展示的地方,突然某个地方长出了一长串的尾数(3.2元打个8折,本来预期展示2.56元,结果显示2.5600000000000005元,用户恐怕是要被吓跑的)。 这属于…

小伙伴经常会遇到类似0.1+0.2===0.3返回false的问题,这种问题非常隐晦。有时候又很明显,比如在价格展示的地方,突然某个地方长出了一长串的尾数(3.2元打个8折,本来预期展示2.56元,结果显示2.5600000000000005元,用户恐怕是要被吓跑的)。

为什么

计算能力那么强的电脑,为什么会出现这种低级错误呢?

这属于底层的问题,我们先看一下js是怎么表示数值的。具体可以参考这篇文章:《为什么0.1+0.2不等于0.3?》 ,原因部分就不再赘述,下面,说一下我们可以怎么用工具/代码进行分析。

我们可以通过toString方法得到数值的二进制表示。

(0.1).toString(2); // "0.0001100110011001100110011001100110011001100110011001101"
(0.2).toString(2); // "0.001100110011001100110011001100110011001100110011001101"

“0.0001100110011001100110011001100110011001100110011001101”对应的是0.1吗?我们找一个带小数的二进制转十进制工具确认一下。 计算机二进制表示的0.1约等于0.10000000000000000555(21个数字),0.2约等于0.2000000000000000111(20个数字) 结合十进制转二进制的算法,可以推断出,0.1在二进制中是无限循环小数(循环节是0011),计算机用双精度表示时做了舍入的近似,于是产生了舍入误差(round-off error)。 通过以上工具分析,我们也得到了相同的结论。

如何解决

解决精度问题的库不少,如decimal.js, 可为 JavaScript提供十进制类型的任意精度数值。 官网:mikemcl.github.io/decimal.js/ GitHub:github.com/MikeMcl/dec…

实际上,需要用到非常大的数字同时要求非常高的精度,情形并不多,而且已经存在现成的“轮子”。更多的是像:0.1+0.23.2*0.8之类的场景。而这些场景理论上是可以通过控制合理的精度进行四舍五入去避免的。如果一两个函数能解决,就没有必要引入一个库了。当然,这里隐含了一个前提:这个函数要足够健壮,同时性能良好。

首先要解决的问题是,合理的精度应该是多少?根据IEEE 754,我们知道,一个64位的双精度浮点数的有效数字大约是16个(十进制, (Math.pow(2,53)+”).length)),2个数进行四则运算后,结果的有效数字大约是15个,我们做四舍五入保留15个数字就可以满足要求。

第一个版本

function fixPrecision(num) {
  var pointIndex = ("" + num).indexOf(".");
  if (num < 0) {
    pointIndex--;
  }
  var ans = (+num).toFixed(15 - pointIndex); // 根据整数长度部分,动态调整精度
  return parseFloat(ans);
}
fixPrecision(0.1+0.2); // 0.3

在单元测试中发现一个反例:fixPrecision(1000.9-1000.6) !== 0.3 为什么呢? 我们直接在命令行把1000.9-1000.6的结果打印出来发现是:0.2999999999999545。我们增加整数的个数看看有没有什么规律

0.9-0.6               // 0.30000000000000004 (输入的整数部分0位,结果的有效数字不超过16位)
1.9-1.6               // 0.2999999999999998 (输入的整数1位,结果的有效数字不超过15位)
10.9-10.6             // 0.3000000000000007 (输入的整数2位,结果的有效数字不超过14位)
100.9-100.6           // 0.30000000000001137 (输入的整数3位,结果的有效数字不超过13位)
1000.9-1000.6         // 0.2999999999999545 (输入的整数4位,结果的有效数字不超过13位)
10000.9-10000.6       // 0.2999999999992724 (输入的整数5位,结果的有效数字不超过12位)
100000.9-100000.6     // 0.29999999998835847 (输入的整数6位,结果的有效数字不超过10位)
1000000.9-1000000.6   // 0.30000000004656613 (输入的整数7位,结果的有效数字不超过10位)
10000000.9-10000000.6 // 0.30000000074505806 (输入的整数8位,结果的有效数字不超过8位)

这里的有效数字指的是调用toFixed能得到预期结果的最大输入数字。 规律还是比较明显的,输入的整数部分每增加1位,结果小数部分的有效数字就相应减少约1位。这样看,网上有些实现是通过把小数转换成整数,计算完再转回对应小数,本质上对精度是没有提升的。道理也很明显,0.1*10变成1,即从一个不能精准表示的数字变成一个能精准表示的数字,中间必然存在舍入。

所以我们,对于加减法,有效数字的处理可以优化成:15 – Math.max(输入数字1的整数个数, 输入数字2的整数个数, 输出数字的整数个数)。

输出数字的精度受到输入数字的影响,这个问题,在乘法和除法中不存在。由于除法可以用乘法表示,我们这里只讨论乘法。比如0.1*0.2,如果计算机用a表示0.1,b表示误差,即 0.1 = a+b,那么0.1*0.2=(a+b)*2*(a+b) = 2a^2 + 4ab + 2b^2,计算机算的是a*2*a 误差是4ab + 2b^2=(4a+2b)*b由于a约等于0.1,b约等于0,所以(4a+2b)<1,所以误差小于b,即比原来任意一个乘数的误差还小。

以上是一个特殊的场景,我们也可以用一种近似的方法来证明一种通用场景。上面我们有结论:有效数字与整数的数量级相关。我们近似表示,被乘数10^a+10^(a-16),乘数10^b+10^(b-16),其中a、b分别表示整数对应的数量级(如,10对应的a=1,0.1对应的a=-1,10^a表示有效值,10^(a-16)表示误差)。那么两数相乘结果是10^(a+b)+2*10^(a+b-16)+10^(b+a-32),其中10^(a+b)为结果的整数部分,误差数量级为10^Math.max(b+a-16, b+a-32) = 10^(b + a - 16)。由此,可以看出对于乘法和除法,修复精度误差只需要考虑输出数据的整数部分数字长度即可。

第二个版本

// 适合处理长度不超过15个数字的场景。(15个数字即整数的位数加上小数的位数不超过15位,
// 如:1234567890.12345 或者 12345.0123456789,之类)
/** *修复精度 * * @param {number} num 需修复的浮点数 * @param {number|undefined} intLength 计算前入参的整数长度部分,用于动态调整精度 * @returns */
function fixPrecision(num, intLength) {
  // 根据整数长度部分,动态调整精度
  return +(+num).toFixed(15 - Math.max(intLength || 0, getIntLength(num)));
}
/** * 获取整数部分数字长度 * * @param {number} num * @returns {number} */
function getIntLength(num) {
  var pointIndex = ("" + num).indexOf(".");
  return num < 0 ? pointIndex - 1 : pointIndex;
}
function getMaxIL(a, b) {
  return Math.max(getIntLength(a), getIntLength(b));
}


function add(a, b) {
    return fixPrecision(a + b, getMaxIL(a, b));
}

add(1000.9, -1000.6); // 0.3

在benchmark性能上,四则运算比mathjs、Decimaljs或网上的一些方法快一个数量级。具体看git项目:fix-precision

参考文献

[01] 《JavaScript 浮点数运算的精度问题》 www.html.cn/archives/73…

[02] 《JavaScript 浮点数陷阱及解法》/ camsong github.com/camsong/blo…

[03] mathjs.org/docs/dataty…

[04] 《二进制转化十进制转换器 带小数》 www.ab126.com/system/7348…

[05] 《IEEE 754 – Standard binary arithmetic float》 www.softelectro.ru/ieee754_en.…

[06] 《IEEE-754 Floating Point Converter》www.h-schmidt.net/FloatConver…

项目地址

github.com/ljquan/fix-…

今天的文章JS中如何便捷地修复精度误差?分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21493.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注