问题

浮点数在运算过程中常常会丢失精度,这是由于二进制数的存储特点造成的,在php或者js中进行浮点数运算或者类型转换的时候常常会丢失精度。而在电商公司,对金额比较敏感,是万万不能接受丝毫的误差的。
看下面这段代码,它的运行结果分别是什么呢?



$var1 = 298.90;
$var2 = $var1 * 100;
$var3 = (int)$var2;
$var4 = (string)$var2;

echo $var2;
echo $var3;
echo $var4;



你的答案可能是



29890
29890
29890



如果真是这样,也就没必要特意提出来说了,其实运行结果是这样的



29890
29889
29890



为什么第二个值变成了29889呢?这和预期不符

让我们来分析一下这个问题,下面这段js代码,可以copy到控制台运行一下



var a = 289.90;
console.log(a * 100);
console.log(parseInt(a * 100));
console.log(a * 100 + '');



运行结果如下



28989.999999999996
28989
28989.999999999996



运行上面这段js代码,结果是浮点数经过乘法运算之后得出的值已经是略小于真实值了,原因是计算机是以二进制数处理数字的,进行运算之后由于长度限制会丢失精度。而经过强制类型转换,变成整型会截取非数字前的部分,就比如运行下面的代码结果会是数值289和数值-289。



var a = '289abc';
var b = '-289abc';
console.log(parseInt(a));
console.log(parseInt(b));



运行结果



289
-289



这样就可以解释为啥开头的例子里第二个数是29889了。

再来看另一个例子



$var1 = 298.90;
$var2 = $var1 * 100;

$var3 = (int)($var2.'');

$var4 = (string)$var2;

echo $var2;
echo $var3;
echo $var4;



运行结果是



29890
29890
29890



就因为转化成了字符串,就一切如常了。

为什么会这样呢?这里我也不太清楚原理,查阅资料也没有弄清楚,希望有知道的同学留言解答一下!

那精度丢失的问题到底有多严重,什么时候我们需要注意呢?我们可以写几个demo来大概了解一下。最常遇到的运算就是“元”和“分”的相互转换。

在js里将0.01元 到100元之间的10000个数值,分别转化成分



var right = 0,error = 0,j = 100;
for(var i = 0;i < 100;i = (parseFloat(i) + 0.01).toFixed(2)){
    var res = i * j;
    if(res != res.toFixed(2)){
        error ++;
        console.log(i + ' * ' + j + ' = ' + res);
    }else{
        right ++;
    }
}
console.log('right: ' + right);
console.log('error: ' + error);
console.log('over');



结果如下



...
81.46 * 100 = 8145.999999999999
81.49 * 100 = 8148.999999999999
81.51 * 100 = 8151.000000000001
81.54 * 100 = 8154.000000000001
81.57 * 100 = 8156.999999999999
81.60 * 100 = 8159.999999999999
81.65 * 100 = 8165.000000000001
81.68 * 100 = 8168.000000000001
81.71 * 100 = 8170.999999999999
81.74 * 100 = 8173.999999999999
81.76 * 100 = 8176.000000000001
81.79 * 100 = 8179.000000000001
81.82 * 100 = 8181.999999999999
81.85 * 100 = 8184.999999999999
81.90 * 100 = 8190.000000000001
right: 8854
error: 1146
over



在js里将1分到10000分之间的10000个数值,分别转化成元



var right = 0,error = 0,j = 100;
for(var i = 0;i < 10000;i = Math.round(parseInt(i) + 1)){
    var res = i / j;
    if(res != res.toFixed(2)){
        error ++;
        console.log(i + ' / ' + j + ' = ' + res);
    }else{
        right ++;
    }
}
console.log('right: ' + right);
console.log('error: ' + error);
console.log('over');



结果如下



right: 10000
error: 0
over



在js里使两个0到1之间的两位小数相减



var right = 0,error = 0;
for(var i = 0;i < 1;i = (parseFloat(i) + 0.01).toFixed(2)){
    for(var j = 0;j < 1;j = (parseFloat(j) + 0.01).toFixed(2)){
        var res = parseFloat(i) - parseFloat(j);
        if(res != res.toFixed(2)){
            error ++;
            console.log(i + ' - ' + j + ' = ' + res);
        }else{
            right ++;
        }
    }
}
console.log('right: ' + right);
console.log('error: ' + error);
console.log('over');



结果如下



...
0.99 - 0.83 = 0.16000000000000003
0.99 - 0.84 = 0.15000000000000002
0.99 - 0.88 = 0.10999999999999999
0.99 - 0.89 = 0.09999999999999998
0.99 - 0.90 = 0.08999999999999997
0.99 - 0.91 = 0.07999999999999996
0.99 - 0.92 = 0.06999999999999995
0.99 - 0.93 = 0.05999999999999994
0.99 - 0.94 = 0.050000000000000044
0.99 - 0.95 = 0.040000000000000036
0.99 - 0.96 = 0.030000000000000027
0.99 - 0.97 = 0.020000000000000018
0.99 - 0.98 = 0.010000000000000009
right: 4844
error: 5156
over



在js里使两个0到1之间的两位小数相加



var right = 0,error = 0;
for(var i = 0;i < 1;i = (parseFloat(i) + 0.01).toFixed(2)){
    for(var j = 0;j < 1;j = (parseFloat(j) + 0.01).toFixed(2)){
        var res = parseFloat(i) + parseFloat(j);
        if(res != res.toFixed(2)){
            error ++;
            console.log(i + ' + ' + j + ' = ' + res);
        }else{
            right ++;
        }
    }
}
console.log('right: ' + right);
console.log('error: ' + error);
console.log('over');



结果如下



...
0.99 + 0.12 = 1.1099999999999999
0.99 + 0.35 = 1.3399999999999999
0.99 + 0.37 = 1.3599999999999999
0.99 + 0.40 = 1.3900000000000001
0.99 + 0.58 = 1.5699999999999998
0.99 + 0.60 = 1.5899999999999999
0.99 + 0.62 = 1.6099999999999999
0.99 + 0.65 = 1.6400000000000001
0.99 + 0.67 = 1.6600000000000001
0.99 + 0.83 = 1.8199999999999998
0.99 + 0.85 = 1.8399999999999999
0.99 + 0.87 = 1.8599999999999999
0.99 + 0.90 = 1.8900000000000001
0.99 + 0.92 = 1.9100000000000001
right: 7894
error: 2106
over



在这些例子里,出错的值占到了很高的比例,但错误值和真实值之间的误差非常小,四舍五入就可以避免。我们在处理数值运算时一定要注意进行处理。

总结

1 如果遇到精度丢失,最简单的办法就是四舍五入



//php方法
$lDefSupPrice = round(79.60 * 100);//取整
$lDefSupPrice = sprintf("%.2f", (0.99 + 0.92));//保留两位小数
//js方法
var fPrice = Math.round(79.60 * 100);//取整
var fPrice = (0.99 + 0.92).toFixed(2);//保留两位小数



2 将整数部分与小数部分分开分别运算,例如



define("float.operation", function(require, exports, module) {
    //加法
    Number.prototype.add = function(arg){
        var r1,r2,m;
        try{r1=this.toString().split(".")[1].length}catch(e){r1=0}
        try{r2=arg.toString().split(".")[1].length}catch(e){r2=0}
        m=Math.pow(10,Math.max(r1,r2))
        return (Number((this*m).toFixed())+Number((arg*m).toFixed()))/m
    }

    //减法
    Number.prototype.sub = function (arg){
        return this.add(-arg);
    }

    //乘法
    Number.prototype.mul = function (arg)
    {
        var m=0,s1=this.toString(),s2=arg.toString();
        try{m+=s1.split(".")[1].length}catch(e){}
        try{m+=s2.split(".")[1].length}catch(e){}
        return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
    }

    //除法
    Number.prototype.div = function (arg){
        var t1=0,t2=0,r1,r2;
        try{t1=this.toString().split(".")[1].length}catch(e){}
        try{t2=arg.toString().split(".")[1].length}catch(e){}
        with(Math){
            r1=Number(this.toString().replace(".",""))
            r2=Number(arg.toString().replace(".",""))
            return (r1/r2)*pow(10,t2-t1);
        }
    }
});



3 如果遇到在调接口的时候php传到接口的时候是准确的,后台读取的时候出错了,可以以字符串的形式来传这个字段,因为PHP是弱类型语言,现在我们的接口大多数情况允许类型不准确

4 在用php处理excel、csv等表格的时候,也可能遇到数据类型的问题,例如生成表格的时候如果以字符串形式存大数字(例如手机号、订单号、身份证号),默认会以科学计数法来显示,甚至身份证号精确度不够直接将后几位置为0了,在前面拼接上空格或英文单引号’以字符串形式输出,在表格里就能正确显示了

5 对于订单号和手机验证码之类可能以0开头的数字,千万不能转整型,另外也不能用 getValueI()方法



$this ->getValueI();



6 对于较大数字的运算,例如解析和拼装后台的属性标,不建议在js中运算,容易溢出,在php中运算会有所改善,附上php解析属性标的方法



/***
 * 解析订单属性lTradeProperty1标签
 * 入参是属性标
 * 回参是一个包含属性标字符串的数组
 */
function getTradeProperty1($val=NULL){
    $skuPropertyNew=array(
        0x000000001 => 'change', //参加以旧换新活动
        0x000000002 => 'coupon', //使用优惠券
        0x000000004 => 'presell', //预售商品(只推迟发货)
        0x000000008 => 'limit', //限时优惠
        0x000000010 => 'score', //积分抵扣
        0x000000020 => 'gift', //礼品券
        0x000000040 => 'newtest', //新品试用
        0x000000080 => 'presellmoney', //预售商品(要交定金)
);
    $arrProperty=array();
    foreach($skuPropertyNew as $k=>$v) {
        if($val & $k) {
            $arrProperty[]=$v;
        }
    }
    return $arrProperty;
}



欢迎补充!



参考:

PHP浮点数的一个常见问题的解答

关于PHP浮点数你应该知道的