2022年1月2日 星期日

PHP json_encode() 小數精度問題

環境:PHP 7.3.12
問題:
$json_arr = ["aa" => 0.1];
echo json_encode($json_arr, JSON_UNESCAPED_UNICODE);
輸出結果
{"aa":0.10000000000000001}
浮點數 0.1 轉成 JSON,變成 0.10000000000000001 

以前沒問題的程式,突然發現有這情況。
原來 PHP 從 7.1 開始,json_encode() 改使用 serialize_precision 的設定值處理小數精度。
而我的環境從 PHP 5.6 慢慢升級到 PHP 7.3.12,設定檔還是沿用舊版的。

舊版使用 precision 設定值處理 json_encode() 的浮點數精度。EG(precision)
7.1版後使用 serialize_precision 設定值處理 json_encode() 的浮點數精度。PG(serialize_precision)

舊版 PHP,php.ini 中 precision = 14,serialize_precision = 17
; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 14

; When floats & doubles are serialized store serialize_precision significant
; digits after the floating point. The default value ensures that when floats
; are decoded with unserialize, the data will remain the same.
serialize_precision = 17

7.1版後,php.ini 中 precision = 14,serialize_precision = -1
; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 14

; When floats & doubles are serialized, store serialize_precision significant
; digits after the floating point. The default value ensures that when floats
; are decoded with unserialize, the data will remain the same.
; The value is also used for json_encode when encoding double values.
; If -1 is used, then dtoa mode 0 is used which automatically select the best
; precision.
serialize_precision = -1



https://bugs.php.net/bug.php?id=74221 上說
 [2017-03-07 23:06 UTC] nikic@php.net
 You can restore the previous behavior by setting serialize_precision to the value of precision,
 which is 14 by default.
 Alternatively and preferably, you can set serialize_precision to -1.
 In this case PHP will automatically determine the minimal precision
 necessary to still accurately represent the number.

解決方法一:將 serialize_precision 設為 14,也就是舊版 json_encode() 處理使用的 precision = 14 這個值
ini_set('serialize_precision', 14);
$json_arr = ["aa" => 0.1];
echo json_encode($json_arr, JSON_UNESCAPED_UNICODE);
輸出結果
{"aa":0.1}

解決方法二:將 serialize_precision 設為 -1,也就是新版的預設值。
ini_set('serialize_precision', -1);
$json_arr = ["aa" => 0.1];
echo json_encode($json_arr, JSON_UNESCAPED_UNICODE);
輸出結果
{"aa":0.1}


precision、serialize_precision 都可設定為 -1,官網說明是會使用較佳的算法。
https://www.php.net/manual/en/ini.core.php#ini.precision
precision int
The number of significant digits displayed in floating point numbers.
-1 means that an enhanced algorithm for rounding such numbers will be used.
https://www.php.net/manual/en/ini.core.php#ini.serialize-precision
serialize_precision int
The number of significant digits stored while serializing floating point numbers.
-1 means that an enhanced algorithm for rounding such numbers will be used.
https://wiki.php.net/rfc/precise_float_value
This RFC proposes to introduce a new setting EG(precision)=-1 and PG(serialize_precision)=-1
 that uses zend_dtoa()'s mode 0 which uses better algorigthm for rounding float numbers
 (-1 is used to indicate 0 mode).
The RFC also proposes changing ini for JSON precision to PG(serialize_precision).



結論:
設成新版預設值 serialize_precision = -1,應該是最佳的解法。

如果不放心,怕又遇到非預期情況,且可接受使用字串形式,也可先將浮點數轉成字串再處理。
$json_arr = ["aa" => (string) 0.1];
echo json_encode($json_arr, JSON_UNESCAPED_UNICODE);
輸出結果
{"aa":"0.1"}





參考:

沒有留言:

張貼留言