php關於金額比較引發的問題(轉)

zchd發表於2016-06-01

做電子商務的時候一般會涉及到金額的比較,按正常的思路來看用><=這些個符號就可以了。可是要是到程式上來搞這個的話就出大事了。現在看下這段程式碼:

$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即使計算到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);
 

bcadd — 將兩個高精度數字相加

  bccomp — 比較兩個高精度數字,返回-1, 0, 1

  bcdiv — 將兩個高精度數字相除

  bcmod — 求高精度數字餘數

  bcmul — 將兩個高精度數字相乘

  bcpow — 求高精度數字乘方

  bcpowmod — 求高精度數字乘方求模,數論裡非常常用

  bcscale — 配置預設小數點位數,相當於就是Linux bc中的”scale=”

  bcsqrt — 求高精度數字平方根

  bcsub — 將兩個高精度數字相減

  整理了一些例項

  php BC高精確度函式庫包含了:相加,比較,相除,相減,求餘,相乘,n次方,配置預設小數點數目,求平方。這些函式在涉及到有關金錢計算時比較有用,比如電商的價格計算。

 

/**
  * 兩個高精度數比較
  * 
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精確到的小數點位數
  * 
  * @return int $left==$right 返回 0 | $left<$right 返回 -1 | $left>$right 返回 1
  */
var_dump(bccomp($left=4.45, $right=5.54, 2));
// -1
  
 /**
  * 兩個高精度數相加
  * 
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精確到的小數點位數
  * 
  * @return string 
  */
var_dump(bcadd($left=1.0321456, $right=0.0243456, 2));
//1.04
 
  /**
  * 兩個高精度數相減
  * 
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精確到的小數點位數
  * 
  * @return string 
  */
var_dump(bcsub($left=1.0321456, $right=3.0123456, 2));
//-1.98
  
 /**
  * 兩個高精度數相除
  * 
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精確到的小數點位數
  * 
  * @return string 
  */
var_dump(bcdiv($left=6, $right=5, 2));
//1.20
 
 /**
  * 兩個高精度數相乘
  * 
  * @access global
  * @param float $left
  * @param float $right
  * @param int $scale 精確到的小數點位數
  * 
  * @return string 
  */
var_dump(bcmul($left=3.1415926, $right=2.4569874566, 2));
//7.71
 
 /**
  * 設定bc函式的小數點位數
  * 
  * @access global
  * @param int $scale 精確到的小數點位數
  * 
  * @return void 
  */ 
bcscale(3);
var_dump(bcdiv(`105`, `6.55957`)); 
// 16.007

 

 


相關文章