问题
浮点数在运算过程中常常会丢失精度,这是由于二进制数的存储特点造成的,在php或者js中进行浮点数运算或者类型转换的时候常常会丢失精度。而在电商公司,对金额比较敏感,是万万不能接受丝毫的误差的。
看下面这段代码,它的运行结果分别是什么呢?1
2
3
4
5
6
7
8$var1 = 298.90;
$var2 = $var1 * 100;
$var3 = (int)$var2;
$var4 = (string)$var2;
echo $var2;
echo $var3;
echo $var4;
你的答案可能是
1 | 29890 |
如果真是这样,也就没必要特意提出来说了,其实运行结果是这样的
1 | 29890 |
为什么第二个值变成了29889呢?这和预期不符
让我们来分析一下这个问题,下面这段js代码,可以copy到控制台运行一下
1 | var a = 289.90; |
运行结果如下1
2
328989.999999999996
28989
28989.999999999996
运行上面这段js代码,结果是浮点数经过乘法运算之后得出的值已经是略小于真实值了,原因是计算机是以二进制数处理数字的,进行运算之后由于长度限制会丢失精度。而经过强制类型转换,变成整型会截取非数字前的部分,就比如运行下面的代码结果会是数值289和数值-289。1
2
3
4var a = '289abc';
var b = '-289abc';
console.log(parseInt(a));
console.log(parseInt(b));
运行结果1
2289
-289
这样就可以解释为啥开头的例子里第二个数是29889了。
再来看另一个例子
1 | $var1 = 298.90; |
运行结果是1
2
329890
29890
29890
就因为转化成了字符串,就一切如常了。
为什么会这样呢?这里我也不太清楚原理,查阅资料也没有弄清楚,希望有知道的同学留言解答一下!
那精度丢失的问题到底有多严重,什么时候我们需要注意呢?我们可以写几个demo来大概了解一下。最常遇到的运算就是“元”和“分”的相互转换。
在js里将0.01元 到100元之间的10000个数值,分别转化成分1
2
3
4
5
6
7
8
9
10
11
12
13var 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');
结果如下
1 | ... |
在js里将1分到10000分之间的10000个数值,分别转化成元
1 | var right = 0,error = 0,j = 100; |
结果如下
1 | right: 10000 |
在js里使两个0到1之间的两位小数相减
1 | var right = 0,error = 0; |
结果如下
1 | ... |
在js里使两个0到1之间的两位小数相加1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var 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');
结果如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18...
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 如果遇到精度丢失,最简单的办法就是四舍五入1
2
3
4
5
6//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 将整数部分与小数部分分开分别运算,例如
1 | define("float.operation", function(require, exports, module) { |
3 如果遇到在调接口的时候php传到接口的时候是准确的,后台读取的时候出错了,可以以字符串的形式来传这个字段,因为PHP是弱类型语言,现在我们的接口大多数情况允许类型不准确
4 在用php处理excel、csv等表格的时候,也可能遇到数据类型的问题,例如生成表格的时候如果以字符串形式存大数字(例如手机号、订单号、身份证号),默认会以科学计数法来显示,甚至身份证号精确度不够直接将后几位置为0了,在前面拼接上空格或英文单引号’以字符串形式输出,在表格里就能正确显示了
5 对于订单号和手机验证码之类可能以0开头的数字,千万不能转整型,另外也不能用 getValueI()方法1
$this ->getValueI();
6 对于较大数字的运算,例如解析和拼装后台的属性标,不建议在js中运算,容易溢出,在php中运算会有所改善,附上php解析属性标的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/***
* 解析订单属性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;
}
1 | 欢迎补充! |
参考: