前言
JS是一门弱语言,在进行保留有效数字时,特别是对于金额等要求较高的计算时,其诡异的处理是我们需要考虑的重点,一般的使用四舍五入即可,这里记录的是在研究时碰见的toFixed的诡异误差而引起的思考
toFixed方法
官方介绍是:一个四舍六入五取偶的方法(也叫银行家算法)
“四舍六入五取偶”
对于位数很多的近似数,当有效位数确定后,其后面多余的数字应该舍去,只保留有效数字最末一位
- “四”是指≤4 时舍去,"六"是指≥6时进上;
- "五"指的是根据5后面的数字来定:
- 当5后有有效数字时,舍5入1;
- 当5后无有效数字时,需要分两种情况来讲:
- 5前为奇数,舍5入1;
- 5前为偶数,舍5不进。(0是偶数)
问题
在不同浏览器中会有不同的表现,这在实际使用中对于用户是极度不友好的
var a = 1.335;
console.log(a.toFixed(2))
===================
// IE 1.34
//chorme 1.33
而单在chorme中测试,发现其结果也是与描述不符的,这里列举一些尝试内容
var b = 1.335
b.toFixed(2)
"1.33"
var b = 1.345
b.toFixed(2)
"1.34"
var b = 1.355
b.toFixed(2)
"1.35"
var b = 1.365
b.toFixed(2)
"1.36"
var b = 1.375
b.toFixed(2)
"1.38"
var b = 1.385
b.toFixed(2)
"1.39"
银行家算法实现
这里简单地提供了一种实现方式
/**
* 银行家算法(四舍六入五取偶) 保留1-14
* @param value 被截取的数字
* @param num 保留小数的位数 使用parseFloat 最多 1到14(第16位小数会截取,15准确,但是判断需要用到身后一位)
*/
var bankCalculate = function (value, num) {
//参数校验
value = parseFloat(value);
if (value !== value) {
console.log("bankCaculate参数非数值");
return value;
}
value = value + "";
num = parseInt(num) !== parseInt(num) ? 0 : parseInt(num);
if (num < 1) num = 1;
if (num > 14) num = 14;
//分离整数与小数
var intValue = value.split(".")[0];
var floatValue = value.split(".")[1];
//如果小数部分长度少于或者等于要保留的位数 直接返回toFixed值即可
if (!floatValue || floatValue.length <= num) return parseFloat(value).toFixed(num);
//计算返回值
var result = parseFloat(intValue + "." + floatValue.substring(0, num));
var toCheck = floatValue.substring(num);
if(parseInt(toCheck.substring(0,1)) < 5) return result + "";
//如果小数部分长度仅比保留位数多一位 代表后位为0
if (floatValue.length === num + 1) {
var prev = floatValue.substring(num - 1, num);
if(parseInt(toCheck) === 5 && prev % 2 === 0){
return result + "";
}
}
//浮点数值直接相加会有精度丢失,这里的accAdd是一个写好的加法运算
return accAdd(result, Math.pow(10,0 - num)) + "";
};
扩展
对于浮点数,直接在JavaScript中进行加减乘除四则运算会出现很大的误差。
原因
计算机中将十进制的数据转化为二进制进行计算,而对于浮点型的数据,转化成二进制之后可能会变成一个无限循环的数字,这在计算机中是不允许的,所以就要对数据做截断,此时的数据运算之后再转成十进制就可能会产生极大的误差。
PS:并不是只JS存在这种问题,Java、C++也都如此,只不过他们内部都有封装好的方法来解决这个问题。JS是一门弱语言,没有做类似的操作。
解决方式
这里仅提供思路,我们应学习问题的解决方式,而不是背代码
//以上面代码中使用parseFloat与parseInt举例
parseInt(999999999999999)//999999999999999 15位
parseInt(9999999999999991)//9999999999999992 16位
999999999999999 + 999999999999999//1999999999999998
//可以看到15位内的整数加法是准确的(实际应该是Math.pow(2, 53)即9007199254740992)
//而上面说过parseFloat的小数部分也是15位精确的,那么我们就可以将其先去除小数再进行运算后恢复小数即可
function add(num1, num2) {
var r1, r2, m;
try {
r1 = num1.toString().split('.')[1].length;
} catch(e) {
r1 = 0;
}
try {
r2 = num2.toString().split(".")[1].length;
} catch(e) {
r2 = 0;
}
m = Math.pow(10,Math.max(r1,r2));
return (num1*m+num2*m)/m;
}
附注
看到这里大家应该会发现,上面的两段代码还是有问题的,对于parseFloat转换后,有效数字超过15位的值仍然有问题,这里不再继续深入说明,以上只是一些对于该问题的思考与拙见
感谢您的阅读!