做电子商务的时候一般会涉及到金额的比较,按正常的思路来看用><=这些个符号就可以了。可是要是到程序上来搞这个的话就出大事了。现在看下这段代码:
$f = 0.07;
var_dump($f * 100 == 7);//输出false
输出结果会出乎大家意料,输出false,为什么会这样呢?其实这个和电脑中存储小数的原理有关。大家都知道计算机只能存储0和1,我们日常生活习惯使用的是10进制的数据,像0.07这个小数在计算机中存储时会有精度损失,以至于计算出来的结果会有偏差。
那么怎么解决这个问题?虽然计算机存储小数有偏差,但是偏差还是非常小,像上例中0.07 * 100如果显示出小数点后面20位的话,最终的值如下:
$f = 0.07;
//输出7.00000000000000088818
echo number_format($f * 100, 20);
可以看到已经在小数点10多位之后了。在实际中我们通常也不需要精确到后面这么多位数字,在金额方面通常精确到后面3位就好了。如果精确到小数点后面三位的话,0.07*100和7就会相等了。在php中提供了一个bccomp函数用来处理这方面的比较。
$f = 0.07;
var_dump($f * 100 == 7);
//输出0,表示两个数字精度为小数点后3位的时候相等
var_dump(bccomp($f * 100, 7, 3));
虽然最终解决了问题,但是还是想搞明白为什么0.07这样的浮点数会有精度损失,经过一段时间的研究,发现产生误差的原因:就在于浮点数的小数位在转换成二进制的时候产生的。
浮点数小数部分转换成二进制规则:乘2取整法,即每一步将十进制小数部分乘以2,所得积的小数点左边的数字(0或1)作为二进制表示法中的数字,直到满足精确度为止。
0.07这个小数的转换成二进制过程如下:
浮点数小数部分转二进制
经过计算发现0.07即使计算到60位后,依然还没有结束。在计算机中,32位的计算机中浮点数尾数部分是23位,64位的是52位。所以后面多出来的部分就会被舍弃掉。
附:
测试都是在64位机器上进行的。
转换小数部分为二进制的代码:
$bin = "";
$int = 7;
$base = 100;
echo "
echo "
位数";echo "
x2";echo "
位值";for ($i = 0; $i <= 60; $i++) {
echo "
";echo "
$i";$int = $int * 2;
echo "
$int";if ($int == 100) {
$bin.="1";
echo "
1";break;
}
if ($int > 100) {
$bin.="1";
$int = $int - $base;
echo "
1";} else {
$bin .= "0";
echo "
0";}
echo "";
echo "
";}
echo "
";echo $bin;
对上例转换的二进制进行反推:
/*
输出内容
0.070000000000000006661338147751
0.070000000000000006661338147751
*/
$f = 0.0;
$bin = "0001000111101011100001010001111010111000010100011110101110000";
$l = strlen($bin);
for ($i = 0; $i < $l; $i++) {
if ($bin[$i] > 0) {
$f = $f + pow(2, -($i + 1));
}
}
echo number_format($f, 30);
$f = 0.07;
echo "
";
echo number_format($f, 30);
参考: