解决toFixed四舍五入陷阱
类别: 技术·JS
时间:2018-05-13 01:19:43
字数:4283
版权所有,未经允许,请勿转载,谢谢合作~
### 四舍五入的不公平
四舍五入是一种精确度的计数保留法,常常用在省略小数点位数时,当所需省略的小数位是[01234]则直接省略,[56789]时进1再省略。
如果我们先精确的得到三位小数的数,需要保留两位小数,并且这个钱是我们付给别人的,简单的使用四舍五入的话,0至9的可能性都是1/10, 求权: (0.000+0.001+0.002+0.003+0.004-0.005-0.004-0.003-0.002-0.001)x0.1 = -0.0005 即我们实际每成交一笔就会亏损 0.0005。
实际情况可能我们无法得到精确的三位小数的,并且我们也无需求权,从数对称可知,每当有需保留m位的小数 0.mx被直接省略,就一定会有一个不同的数 1-0.mx 进1到第上一位后再省略x,除非这个x是5开头并且在它之后只有0,这时对称后是它自己,这也就是不公平所在。
### 银行家舍入
为了解决四舍五入的不公平,有人提出银行家传入,也就是四舍六入,五看情况。
所谓看情况,就是处理不公平的特殊所在,即被舍入如果是5并且5之后只有0时,此时看5上一位是偶直接舍去,为奇进1再舍去。比如:
13.1249999999 变 13.12
13.1250000001 变 13.13
13.1250000000 变 13.12
13.1350000000 变 13.14
### toFixed函数
NumberObject.toFixed(num)
在JS中,对NumberObject进行“四舍五入”,num为保留的小数个数,默认不填为0,最后返回一个数字的字符串。
我们知道了四舍五入不公平,但我们偏偏就想用不公平的四舍五入可以用这个函数获取到偏偏想到的结果吗?
不能!
比如:
0.105.toFixed(2) === '0.10'
0.115.toFixed(2) === '0.12'
0.125.toFixed(2) === '0.13'
0.135.toFixed(2) === '0.14'
0.145.toFixed(2) === '0.14'
0.155.toFixed(2) === '0.15'
……
可以看到toFixed既没有使用银行家舍入,也没有得到我们想要的四舍五入结果,有一种瞎搞的即视感。
如果了解数的存储就会知道原因所在,我们看到的数在存储中并可能不是这个数,而是可能一个更大的数,也可能是一个更小的数,因为二进制精确度有限。
如果不了解数的存储,请先看[西法](http://www.boatsky.com "太空船博客" rel="nofollow")的另一篇文章:[深入理解IEEE754的64位双精度](http://www.boatsky.com/blog/26.html " 深入理解IEEE754的64位双精度" rel="nofollow")
这里写一个test方法,最多可以拿小数100多个尾数的二进制方法:
````javascript
// 转换正数的小数成二进制
function transformBinary(source) {
let binary = '';
let decimalsArr,integer,decimals;
for(let i = 0;i < 107;i++) {
decimalsArr = source.toString().split('.');
integer = decimalsArr[0];
decimals = decimalsArr[1];
if (decimals !== undefined) {
if (i > 0) {
binary = binary + integer;
source = parseFloat('0.' + decimals);
}
source = source * 2;
} else {
binary = binary + integer;
break;
}
}
}
let n = 0.105;
console.log(transformBinary(n));
````
可得到0.105的二进制为
````javascript
'0.0001101011100001010001111010111000010100011110101110000101000111101011100001010001111010111000010100011110'
// 使用js原生toString(2)方法得到的52个尾数的,注意尾数不等于小数点后个数,而是有效数字。
'0.00011010111000010100011110101110000101000111101011100001'
````
发现前者比后者大,即0.105实际二进制值比0.105小,对应的十进制小数点第三位是4,被直接舍去了。
事实排除二进制精确度问题,toFixed确实是使用四舍五入的。如何[解决toFixed四舍五入陷阱](http://www.boatsky.com/blog/32.html "解决toFixed四舍五入陷阱" rel="nofollow")?
### 解决四舍五入的陷阱
我们自己实现一个银行家舍入:
```javascript
Number.prototype.toFixedPlus = function(num) {
// 严格限定只支持 \d 正整型数字
num = /^\d+$/.test(num) ? Number(num) : 0;
// 我们需要操作的数字,它只会是正数,符号是不会进入参加计算的
let valStr = Number(this).toString();
// 小数点的位置,也时也是整数部分与符号部分的长度
let pointIndex = valStr.indexOf('.');
// 获取数字的最后一位
function _lastNumber(vStr) {
return Number(Number(vStr).toString().substr(-1, 1));
}
// 进位,虽然这个过程可能会有误差,但是是接近原数如0.000999999998之类的,可以用toFixed变成0.001了
function _carry(vStr) {
return ((Number(vStr) + Math.pow(10, -num)).toFixed(num)).toString();
}
// 补0, vStr数字, zoreN补0个数, point是否需要'.'
function _supplyZore(vStr, zoreN, point = '') {
return vStr + point + new Array(zoreN + 1).join('0');
}
if (pointIndex >= 0) {
// 最后需要变成的长度,比如1.0015.toFixedPlus(3),那么最后变在 1(整数) + 1(小数点) + 3(保留小数的长度)
let newValLen = pointIndex + 1 + num;
// 不考虑进位时,返回的结果,即前半部分,比如1.0015的前半部分是0.001
let preStr = valStr.substr(0, newValLen);
// 用来判断是否需进位的后半部分,如1.0015的后半部分是5
let nextStr = valStr.substr(newValLen);
// 后半部分的第一位数字
let nextFirstN = nextStr ? Number(nextStr.substr(0, 1)) : 0;
switch(true) {
// 四舍去
case nextFirstN <= 4 :
valStr = preStr;
break;
// 五判断各类情况再处理
case nextFirstN === 5 :
if (/^5[0]*$/.test(nextStr)) {
// preStr可能会是 1. 这种形式,除了先转成1,再取得前半部最后一个数字
let preLastN = _lastNumber(preStr);
if (preLastN % 2 === 1) {
valStr = _carry(preStr);
} else {
valStr = preStr;
}
} else {
valStr = _carry(preStr);
}
break;
// 六进位
case nextFirstN >= 6 :
valStr = _carry(preStr);
break;
}
// 如果有小数变成无小数,去除多余的小数点
if (num === 0) {
valStr = valStr.replace('.','');
} else {
// 小数时,长度差额,补0个数
let lessZore = newValLen - valStr.length;
if (lessZore >= 1) {
valStr = _supplyZore(valStr, lessZore);
}
}
} else {
// 没有小数点时补0
if (num >= 1) {
valStr = _supplyZore(valStr, num, '.');
}
}
return valStr;
}
```
有了它,就可以摈弃四舍五入,愉快的使用公平的银行家舍入了,理是这个理,可有的时候资本家不仅不使用这种公平的方式,连不公平的四舍五入也省了,而是更粗暴的把末尾直接抹去(笑)。
“Now I have come to the crossroads in my life.I always knew what the right path was. Without exception, I knew, but I never took it. You know why? It was too damn hard.”——《闻香识女人》
全部留言
我要留言
内容:
网名:
邮箱:
个人网站:
发表