注册
web

为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

前言


很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


什么是银行家舍入法


银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


具体操作步骤如下:



  1. 如果被修约的数字小于5,则直接舍去;
  2. 如果被修约的数字大于5,则进行进位;
  3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。

以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


举例


在浏览器的控制台中,我们可以试着打印一下


image.png
这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


image.png
这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


image.png
这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


image.png
现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


解决方法


先说一种可行但不完全可行的解决方法,就是使用Math.round()
首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


image.png
哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


image.png
呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


image.png
可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


image.png
这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


当然还有一个方法就是自己写一个方法来解决这个问题


//有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
Number.prototype.myToFixed = function (n, d) {
//进来之后转为字符串 字符串不存在精度问题
const str = this.toString();
const dotIndex = str.indexOf(".");
//如果没有小数点传进来的就是整数,直接使用toFixed传出去
if (dotIndex === -1) {
return this.toFixed(n);
}
//当为小数的时候
const intStr = str.substring(0, dotIndex);
const decStr = str.substring(dotIndex + 1, str.length).split("");
//当大于5时,就进一
if (decStr[n] >= 5) {
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//否则小于五时 先判断是否有第二个参数
if (d) {
//如果有就截取到第二个参数的位置
const newDec = decStr.splice(n, n + d);
let nineSum = 0;
//遍历循环有多少个9
for (let index = 0; index < newDec.length; index++) {
if (index != 0 && newDec[index] == 9) {
nineSum++;
}
}
//判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
//条件成立 就按5进一
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//不成立则舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
} else {
//没有第二个参数,小于五直接舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
}
};

我们再进行测试一下


image.png


image.png
这样就是我们想要的结果了


总结


在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


写的如有问题,欢迎提出建议


作者:iceCode
来源:juejin.cn/post/7280430881952759862

0 个评论

要回复文章请先登录注册