今天來學習的是關于數(shù)學方面的第一個擴展。對于數(shù)學操作來說,無非就是那些各種各樣的數(shù)學運算,當然,整個程序軟件的開發(fā)過程中,數(shù)學運算也是最基礎最根本的東西之一。不管你是學得什么專業(yè),到最后基本上都會要學習數(shù)據(jù)結構與算法,而算法其實就是研究的如何利用數(shù)學來優(yōu)化各種排序和查找能力。PHP 在底層已經(jīng)幫我們準備好了很多的數(shù)學計算函數(shù),就讓我們一一來學習吧。
什么是精度問題
關于精度問題,可能很多做過金融方面的小伙伴都不會陌生。特別是前端的同學,如果你在 js 中執(zhí)行 1.1+2.2 ,獲得的結果往往不會如你所愿。這就要說到浮點數(shù)的存儲問題了。我們都知道,在程序世界中,任何數(shù)據(jù)其實在底層都是以二進制的形式存在的。而浮點數(shù),則由于小數(shù)點的存在,在存儲時更為復雜,所以就會經(jīng)常出現(xiàn)這類精度丟失的問題。
但是很多人會很奇怪,在 PHP 中直接執(zhí)行 1.1+2.2 的結果是正確的呀,好像并不存在這種精度丟失的問題。呵呵,那只能說您 too young to simple 了。精度丟失的問題并不是哪個語言的問題,基本上所有語言都會存在這樣的問題,只是表現(xiàn)的形式不一樣。
bc 精度運算
我們先來看一下在 PHP 環(huán)境中的精度丟失要怎么展現(xiàn)出來。
$a = 0.58;
echo $a * 100, PHP_EOL; // 58
echo intval($a * 100), PHP_EOL; // 57
echo (int) ($a * 100), PHP_EOL; // 57
echo intval(bcmul($a, 100)), PHP_EOL; // 58
我們定義了一個變量 $a ,它的內容是 0.58 。這時我們給他直接乘 100 ,結果貌似沒什么問題。但是如果我們將它強轉為 int 類型的話,就出現(xiàn)問題了,明明是 58 ,為什么變成了 57 ?
其實,在浮點運算后,得到的結果并不是 58 ,而是 57.99999999999999 這樣的數(shù),如果我們直接 echo 的話,會經(jīng)過字符串強轉,這個會直接輸出 58 ,但如果是經(jīng)過 int 強轉的話,不管是 inval() 還是 (int) ,都會按照 int 強轉的舍棄小數(shù)的規(guī)則進行轉換。于是,結果就變成了 57 了。
通過直接的 echo 經(jīng)常會讓我們感覺到 PHP 中貌似不會出現(xiàn)精度丟失的問題,但其實這個問題還真是存在的。在很多情況下,比如存入數(shù)據(jù)庫,或者轉換成 json 格式就會發(fā)現(xiàn)問題。如果想要精確地計算,就可以使用 bc 擴展相關的函數(shù),也就是我們最后演示的那個 bcmul() 函數(shù)。它的作用就是第一個參數(shù)乘以第二個參數(shù),獲得的結果也是高精度的,也就是精度準確的結果。
接下來我們通過 json 格式的轉換來看看加減乘除各類情況下的精度問題。
echo json_encode([
'a1' => $a, // "a1":0.58
'a2' => $a * 100, // "a2":57.99999999999999
'a3' => intval($a * 100), // "a3":57
'a4' => floatval($a * 100), // "a4":57.99999999999999
'a5' => floatval($a), // "a5":0.58
'a6' => intval(bcmul($a, 100)), // "a6":58
'a7' => 1.1 + 2.2, // "a7":3.3000000000000003
'a8' => floatval(bcadd(1.1, 2.2, 10)), // "a8":3.3
'a9' => 2 - 1.1, // "a9":0.8999999999999999
'a10' => floatval(bcsub(2, 1.1, 10)), // "a10":0.9
'a11' => floatval($a * 100 / 10), // "a11":5.799999999999999
'a12' => floatval(bcdiv($a * 100, 10, 10)), // "a12":5.8
'a13' => 10 % 2.1, // "a13":0
'a14' => bcmod(10, 2.1), // "a14":"1"
'a15' => pow(1.1, 2), // "a15":1.2100000000000002
'a16' => bcpow(1.1, 2, 30), // "a16":"1.210000000000000000000000000000"
'a17' => sqrt(1.1), // "a17":1.0488088481701516
'a18' => bcsqrt(1.1, 30), // "a18":"1.048808848170151546991453513679"
]), PHP_EOL;
通過這段代碼大家應該就能清楚地看到 PHP 中的精度丟失問題是否存在了。json_encode() 在轉換數(shù)據(jù)的時候會根據(jù)字段的類型進行轉換,所以精度問題會比較明顯,這也是很多同學在后端計算的時候明明沒有問題,但通過 json 輸出到前端就會發(fā)現(xiàn)數(shù)據(jù)發(fā)生了精度問題的原因。
a1~a6 就是我們第一段測試代碼的內容,可以很明顯地看到普通地使用 $a * 100 的結果真的是 57.99999999999999 了吧。
a7、a8 是加法的演示,怎么樣,在 PHP 中,1.1+2.2 的結果其實也和 JS 中是一樣的吧,通過 bcadd() 就可以處理加法的精度問題。同理,a9、a10 是減法的問題,通過 bcsub() 就可以獲得減法的高精度計算結果。bcdiv() 則是用于處理除法。注意,這幾個函數(shù)都有第三個參數(shù),它表示的是保留小數(shù)點的位數(shù),我們都給了保留 10 位小數(shù)點,目的是希望如果出現(xiàn)丟失精度的問題可以和原計算比對。
bcmod() 的余數(shù)計算,對應的也就是 % 計算符號的作用。正常情況下,10 % 2 的結果為 0 是正常的,但這里我們計算的是 10 % 2.1 結果也是 0 ,而在使用 bcmod() 之后,結果為 1 ,這才是正確的結果。bcpow() 是乘方的計算,對應的是普通函數(shù)中的 pow() 函數(shù),同樣在這里我們在普通函數(shù)的計算中 1.1 的 2 次方出現(xiàn)了精度問題,使用 bcpow() 我們顯示 30 位的小數(shù)也沒有找到精度異常。這里需要注意的是,bcpow() 如果指定了小數(shù)位數(shù),是會顯示出來的,即使計算結果是沒有小數(shù)的,也會以 0 全部顯示出來。而上面其它的函數(shù)則不會這樣,只會在確實有小數(shù)的情況下才顯示出來。
最后則是 bcsqrt() 函數(shù),也就是二次方根,這個沒有找到有溢出的數(shù)可以供我們測試,如果有使用過并發(fā)現(xiàn)過溢出的小伙伴可以留言哦。
比較函數(shù)
上面說完了各種精度計算的函數(shù),接下來我們看一下數(shù)字比較的問題。
echo bccomp(1, 2), PHP_EOL; // -1
echo bccomp(1.00001, 1, 3), PHP_EOL; // 0
echo bccomp(1.00001, 1, 5), PHP_EOL; // 1
bccomp() 函數(shù)就是用來根據(jù)小數(shù)點位數(shù)進行精度比較的函數(shù)。它的返回結果是如果參數(shù)1小于參數(shù)2返回 -1 ,大于返回 1,等于則返回 0 。第三個參數(shù)用戶確定比較到哪一位。在這個例子中,我們可以看到,如果只比較到第三位小數(shù)的話,1.00001 和 1 的結果是相等的。而如果比較到第五位小數(shù)的話,它們的差異就體現(xiàn)出來了。
設置小數(shù)點及 bcpowmod 函數(shù)
最后我們再看兩個函數(shù)。
bcscale(30);
echo bcmod(bcpow(5, 2), 2), PHP_EOL; // 1.000000000000000000000000000000
echo bcpowmod(5, 2, 2), PHP_EOL; // 1.000000000000000000000000000000
bcscale() 是在全局設置小數(shù)點的位數(shù)。設置這個函數(shù)后,上面介紹過的所有函數(shù)如果不寫第三個小數(shù)點位數(shù)函數(shù)的話,都會以 bcscale() 設置的為準。
bcpowmod() 函數(shù)的作用就和第二行的測試代碼一樣,就是先進行一次 bcpow() 再進行一次 bcmod() 。它的使用場景不多,不過寫法很方便。
總結
今天的內容除了 bc 相關的計算函數(shù)之外,也講到了精度問題這個各種語言都存在的問題。其實說實話,我們在日常開發(fā)中,對于金額這類帶小數(shù)點的數(shù)據(jù),最好都是以分為單位進行存儲。也就是說,在后臺,保存和計算的數(shù)據(jù)都是整型的數(shù)據(jù),在前端展示的時候,直接除 100 再保留兩位小數(shù)就可以了。這樣就可以極大地保證數(shù)據(jù)的精度不會丟失。
另外,關于 PHP 中精度問題相關的參考大家可以看看下方第二個鏈接中鳥哥博客上的說明。我們的例子 0.58 * 100 也是摘自他的博客中的示例。
測試代碼:
https://github.com/zhangyue0503/dev-blog/blob/master/php/202012/source/7.學習PHP中的任意精度擴展函數(shù).php
參考文檔: