小伙伴常常会遇到相似0.1+0.2===0.3
返回false的问题,这种问题很是隐晦。有时候又很明显,好比在价格展现的地方,忽然某个地方长出了一长串的尾数(3.2元打个8折,原本预期展现2.56元,结果显示2.5600000000000005元
,用户恐怕是要被吓跑的)。html
计算能力那么强的电脑,为何会出现这种低级错误呢?git
这属于底层的问题,咱们先看一下js是怎么表示数值的。具体能够参考这篇文章:《为何0.1+0.2不等于0.3?》 ,缘由部分就再也不赘述,下面,说一下咱们能够怎么用工具/代码进行分析。github
咱们能够经过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)。 经过以上工具分析,咱们也获得了相同的结论。bash
解决精度问题的库很多,如decimal.js, 可为 JavaScript提供十进制类型的任意精度数值。 官网:mikemcl.github.io/decimal.js/ GitHub:github.com/MikeMcl/dec…函数
实际上,须要用到很是大的数字同时要求很是高的精度,情形并很少,并且已经存在现成的“轮子”。更多的是像:0.1+0.2
、3.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…
[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…