做电子商务的时候一般会涉及到金额的比较,按正常的思路来看用><=这些个符号就可以了。可是要是到程序上来搞这个的话就出大事了。现在看下这段代码:
$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 "<table border='1'>"; echo "<td width='50'>位数</td>"; echo "<td width='50'>x2</td>"; echo "<td width='50'>位值</td>"; for ($i = 0; $i <= 60; $i++) { echo "<tr>"; echo "<td>$i</td>"; $int = $int * 2; echo "<td>$int</td>"; if ($int == 100) { $bin.="1"; echo "<td>1</td>"; break; } if ($int > 100) { $bin.="1"; $int = $int - $base; echo "<td>1</td>"; } else { $bin .= "0"; echo "<td>0</td>"; } echo "</td>"; echo "</tr>"; } echo "</table>"; 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 "<br />"; echo number_format($f, 30);
参考:
https://en.wikipedia.org/wiki/IEEE_floating_point
转载请注明:快乐编程 » php关于金额比较引发的问题