PHP中json浮點精度的解決方法
前言
之前開發(fā)的接口需要用到j(luò)son加簽,有一次對接JAVA時,簽名怎么都過不了,仔細對比了字符串,發(fā)現(xiàn)是PHP進行json_encode時,會將浮點型所有無意義的0給去掉(echo和var_dump也會),而JAVA那邊沒有。遂在文檔中寫下: “json中請把無意義的0去掉”。
最近又遇到這個事情,需求直接要求:顯示字符型,且精度要保留兩位小數(shù),于是不得不開始研究PHP的json中,浮點型的精度該如何保留的問題。
解決方案
json_encode常量參數(shù)(無法解決)
相關(guān)知識
json_encode的函數(shù)原型如下:
json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false
眾所周知,json_encode的第一個進階用法,就是它的第二個參數(shù)flags,也就是“可選的json編碼方式”,各種奇妙的常量。比如我最長用到的,JSON_UNESCAPED_UNICODE
,讓json不自動進行unicode轉(zhuǎn)換,直接輸出中文。所以第一個想到的,就是查看有沒有對應(yīng)的常量參數(shù)。
查看源碼,json的常量參數(shù)都放在 php-src/ext/json/php_json.h
中,如下:
/* json_encode() options */ #define PHP_JSON_HEX_TAG (1<<0) #define PHP_JSON_HEX_AMP (1<<1) #define PHP_JSON_HEX_APOS (1<<2) #define PHP_JSON_HEX_QUOT (1<<3) #define PHP_JSON_FORCE_OBJECT (1<<4) #define PHP_JSON_NUMERIC_CHECK (1<<5) #define PHP_JSON_UNESCAPED_SLASHES (1<<6) #define PHP_JSON_PRETTY_PRINT (1<<7) #define PHP_JSON_UNESCAPED_UNICODE (1<<8) #define PHP_JSON_PARTIAL_OUTPUT_ON_ERROR (1<<9) #define PHP_JSON_PRESERVE_ZERO_FRACTION (1<<10) #define PHP_JSON_UNESCAPED_LINE_TERMINATORS (1<<11)
PHP_JSON_UNESCAPED_UNICODE,恰好對應(yīng)的就是256,二進制的設(shè)計是為了他們可以方便的復(fù)合使用。寫法也很多變,比如json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
,json_encode($data, JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES)
,json_encode($data, 256 + 64)
。都是一樣的實現(xiàn)。
其中和數(shù)字有關(guān)的,就是PHP_JSON_NUMERIC_CHECK
,以及PHP_JSON_PRESERVE_ZERO_FRACTION
。
// 將所有數(shù)字字符串編碼成數(shù)字(numbers)。 // Encodes numeric strings as numbers. JSON_NUMERIC_CHECK (int) // 確保 float 值始終編碼為為 float 值。 // Ensures that float values are always encoded as a float value. JSON_PRESERVE_ZERO_FRACTION (int)
做排列組合試驗
$str_arr = [ 'str1' => '1', 'str2' => '1.0', 'str3' => '1.00', 'str4' => '1.1', 'str5' => '1.10', 'str6' => '1.110' ]; $s_j1 = json_encode($str_arr, JSON_NUMERIC_CHECK); $s_j2 = json_encode($str_arr, JSON_PRESERVE_ZERO_FRACTION); $s_j3 = json_encode($str_arr, JSON_NUMERIC_CHECK | JSON_PRESERVE_ZERO_FRACTION); echo $s_j1,PHP_EOL; echo $s_j2,PHP_EOL; echo $s_j3,PHP_EOL; echo PHP_EOL; $float_arr = [ 'f1' => 1, 'f2' => 1.0, 'f3' => 1.00, 'f4' => 1.1, 'f5' => 1.10, 'f6' => 1.110 ]; $f_j1 = json_encode($float_arr, JSON_NUMERIC_CHECK); $f_j2 = json_encode($float_arr, JSON_PRESERVE_ZERO_FRACTION); $f_j3 = json_encode($float_arr, JSON_NUMERIC_CHECK | JSON_PRESERVE_ZERO_FRACTION); echo $f_j1,PHP_EOL; echo $f_j2,PHP_EOL; echo $f_j3,PHP_EOL;
結(jié)果
{"str1":1,"str2":1,"str3":1,"str4":1.1,"str5":1.1}
{"str1":"1","str2":"1.0","str3":"1.00","str4":"1.1","str5":"1.10"}
{"str1":1,"str2":1.0,"str3":1.0,"str4":1.1,"str5":1.1}
{"f1":1,"f2":1,"f3":1,"f4":1.1,"f5":1.1}
{"f1":1,"f2":1.0,"f3":1.0,"f4":1.1,"f5":1.1}
{"f1":1,"f2":1.0,"f3":1.0,"f4":1.1,"f5":1.1}
結(jié)論
可以看到JSON_NUMERIC_CHECK
正如文檔描述中的那樣,將所有數(shù)字字符串都編碼成了數(shù)字,無意義的0仍舊會被處理掉。
而JSON_PRESERVE_ZERO_FRACTION
的表現(xiàn)形式就有些奇怪,只能在有第一位小數(shù)且為0時,只保留一位0。
顯然,flags是無法滿足需求的。
配置項"serialize_precision"("precision")(無法解決)
文檔中有這么一句話
如果參數(shù)是 array 或 object,則會遞歸序列化。
編碼受傳入的 flags 參數(shù)影響,此外浮點值的編碼依賴于 serialize_precision。
serialize_precision int 序列化浮點數(shù)時存儲的有效數(shù)字的位數(shù)。-1 表示將使用增強算法來四舍五入此類數(shù)字。
PHP中,serialize_precision
配置項用于序列化時控制浮點數(shù)的精度,而precision
用于平常顯示時的控制。
我們?nèi)∫粋€數(shù)字,echo json_encode(17.2);
,將serialize_precision
,從低到高設(shè)置。得到下面的結(jié)果:
0 2.0e+1
1 2.0e+1
2 17
3 17.2
4 17.2
可以比較清楚的看出這個配置的效果了,而且顯然,無法達成需求。
題外話:
測試時發(fā)現(xiàn),在PHP7.1以上的版本中,如果將serialize_precision
的數(shù)值設(shè)置為很大,比如5.*
版本默認的17,得到的結(jié)果是: 17.199999999999999
。precision
同理,作用于echo
,var_dump
,print_r
等。
所以建議日常使用,設(shè)置為默認的-1就好。
字符串處理-正則
如此來看,從編碼配置層面似乎無法解決這個需求了,那么就使用最簡單直接的辦法: 用正則,直接對字符串下手。
foreach ($data as &$item) { if (is_numeric($item)) { $item = sprintf("%.2f", $item); } } $json = json_encode($data); // 浮點型轉(zhuǎn)換為數(shù)值型 $pattern = '/"(\d+\.\d+)"/'; $replacement = '$1'; $new_json = preg_replace($pattern, $replacement, $json);
這段函數(shù),是把數(shù)值全部先轉(zhuǎn)換為保留2位小數(shù)的字符串,進行json_encode后,再把字符串中所有帶".",左右是數(shù)字的,外層的雙引號去掉。
如果你的json更為復(fù)雜,需要對正則進行調(diào)整。
原理-PHP中浮點型的顯示
我們來看這么一段代碼,猜測下他的輸出結(jié)果會是什么:
echo 1.0; var_dump(1); var_dump(1.0); var_dump(1.0 === 1); var_dump(1.00 === 1.0);
結(jié)果:
1
int(1)
float(1)
bool(false)
bool(true)
那么,為什么會出現(xiàn)float(1)
,1.00 === 1.0
這樣奇怪的輸出呢?原因在于PHP內(nèi)核中變量容器Zval(Zend value)的實現(xiàn),以及顯示處理。
PHP是一個弱類型語言,一個變量,可以是任何類型,這也得益于Zval的實現(xiàn)。Zval,也就是_zval_struct這個結(jié)構(gòu)體,主要記錄了三塊東西:值,類型,引用計數(shù)。并沒有“顯示精度”這種屬性和配置。(引用計數(shù)和垃圾回收有關(guān))
所以在var_dump時,顯示的是變量的類型float,以及和存儲的值,最近似的有意義的數(shù)值,也就是float(1)。而使用===對比時,存儲的值相等,類型也相等,自然就會顯示成true。
對應(yīng)源碼
a) 對浮點型的輸出函數(shù) smart_str_append_double
// Zend\zend_smart_str.c ZEND_API void ZEND_FASTCALL smart_str_append_double( smart_str *str, double num, int precision, bool zero_fraction) { char buf[ZEND_DOUBLE_MAX_LENGTH]; /* Model snprintf precision behavior. */ zend_gcvt(num, precision ? precision : 1, '.', 'E', buf); smart_str_appends(str, buf); if (zero_fraction && zend_finite(num) && !strchr(buf, '.')) { smart_str_appendl(str, ".0", 2); } }
JSON_PRESERVE_ZERO_FRACTION 是在這里進行的影響,會在最終判斷是否整形,并加".0"
b) smart_str_append_double 的引用部分
// ext\standard\var.c PHPAPI zend_result php_var_export_ex(zval *struc, int level, smart_str *buf) { ... case IS_DOUBLE: smart_str_append_double( buf, Z_DVAL_P(struc), (int) PG(serialize_precision), /* zero_fraction */ true); break; ... }
// Zend\zend_ast.c static ZEND_COLD void zend_ast_export_zval(smart_str *str, zval *zv, int priority, int indent) { ... case IS_DOUBLE: smart_str_append_double( str, Z_DVAL_P(zv), (int) EG(precision), /* zero_fraction */ false); break; ... }
可以很明顯的看到,serialize_precision和precision,就是從這里進行的引入。
c) smart_str_append_double 對浮點型字符串的處理函數(shù): zend_gcvt
// Zend\zend_strtod.c ZEND_API char *zend_gcvt(double value, int ndigit, char dec_point, char exponent, char *buf) { ... if ((decpt >= 0 && decpt > ndigit) || decpt < -3) { /* use E-style */ /* exponential format (e.g. 1.2345e+13) */ ... } else if (decpt < 0) { /* standard format 0. */ *dst++ = '0'; /* zero before decimal point */ *dst++ = dec_point; do { *dst++ = '0'; } while (++decpt < 0); src = digits; while (*src != '\0') { *dst++ = *src++; } *dst = '\0'; } else { /* standard format */ for (i = 0, src = digits; i < decpt; i++) { if (*src != '\0') { *dst++ = *src++; } else { *dst++ = '0'; } } if (*src != '\0') { if (src == digits) { *dst++ = '0'; /* zero before decimal point */ } *dst++ = dec_point; for (i = decpt; digits[i] != '\0'; i++) { *dst++ = digits[i]; } } *dst = '\0'; } zend_freedtoa(digits); return (buf); }
e的寫法,清除無意義的0,在這里被實現(xiàn)。
如何顯示精度
如果要顯示確切的精度,只能轉(zhuǎn)換為字符串類型,有兩種方法:
$number = 1; echo sprintf("%.2f", $number); echo number_format($number, 2, '.', '');
兩種方法都在PHP4的版本實裝,可以放心使用。
需要注意的是,如果本身的位數(shù)超過精度,這兩種方法都會四舍五入。
另外,number_format
的第三個參數(shù)為“小數(shù)點符號”,第四個參數(shù)為“千位分隔符”。默認分別是"."和","。尤其是需要進行數(shù)字計算和正常顯示時,需要注意“千位分隔符”的設(shè)置。
關(guān)于"double"和"float"
PHP中的浮點型,是使用c中的double型實現(xiàn)的,全部都是遵循 IEEE754 標準,64位的雙精度浮點數(shù),不存在單精度。
在PHP中,double和float的命名使用的很混亂。在源碼中,多見double,類型判斷用的也是IS_DOUBLE
。但在7以后,顯示定義的類型,必須使用float。比如 function(float $num): float
,這似乎是為了與其他語言的命名方式保持一致。
獲取類型的相關(guān)函數(shù),使用不同版本進行了簡單測試,很奇怪,盡量別用8.2:
gettype(1.0); // double var_dump(1.0); // 8.2版本顯示為double,8.3及其他版本都是float,同時8.2版本也多出了文件位置的輸出
其他函數(shù):
// 都只是別名,功能一致 is_float(); is_double(); floatval(); doubleval(); ...
浮點型的對比和精確計算
從float文檔中可以看到,由于精度問題,官方是不支持把浮點型進行直接對比和計算的,“永遠不要相信浮點數(shù)結(jié)果精確到了最后一位,也永遠不要比較兩個浮點數(shù)是否相等”
。(例如,0.1 + 0.2 在計算機中并不等于 0.3,而是等于 0.30000000000000004)
一般正常的四則運算其實影響不大,但如果對精度有很高的要求,推薦使用BC系列函數(shù),或者GMP函數(shù)。
對比前,先使用round()
函數(shù),將浮點型進行四舍五入處理。(和官方給的處理方式類似,但更好理解)
$x = 8 - 6.4; // which is equal to 1.6 $y = 1.6; var_dump($x == $y); // is not true PHP thinks that 1.6 (coming from a difference) is not equal to 1.6. To make it work, use round() var_dump(round($x, 2) == round($y, 2)); // this is true This happens probably because $x is not really 1.6, but 1.599999.. and var_dump shows it to you as being 1.6.
float型的下劃線
7.4以后,支持對浮點型添加下劃線,只是增加可讀性,和千分符類似:
1_000.0 == 1000.0; // true
其他
json_encode常量參數(shù)版本適用性
- PHP_JSON_HEX_TAG、PHP_JSON_HEX_AMP、PHP_JSON_HEX_APOS、PHP_JSON_HEX_QUOT、PHP_JSON_FORCE_OBJECT、PHP_JSON_NUMERIC_CHECK、PHP_JSON_UNESCAPED_SLASHES、PHP_JSON_PRETTY_PRINT、PHP_JSON_UNESCAPED_UNICODE:在 PHP 5.3.0 及以上版本可用。
- PHP_JSON_PARTIAL_OUTPUT_ON_ERROR:在 PHP 5.5.0 及以上版本可用。
- PHP_JSON_PRESERVE_ZERO_FRACTION:在 PHP 5.6.6 及以上版本可用。
- PHP_JSON_UNESCAPED_LINE_TERMINATORS:在 PHP 7.3.0 及以上版本可用。
對象的序列化處理-JsonSerializable(json_encode的其他進階用法)
閱讀json_encode文檔時,還可以發(fā)現(xiàn),
實現(xiàn) JsonSerializable 的類可以 在 json_encode() 時定制他們的 JSON 表示法(序列化)。
go的json序列化比較常見,可以結(jié)合理解。
JAVA也有同名JsonSerializable方法,是將類信息也帶入json中,可以實現(xiàn)反序列化,不常用。
class IDou implements JsonSerializable { public function __construct(protected $name, protected $year) {} public function jsonSerialize() { return ['name' => $this->name, 'year' => $this->year]; } } echo json_encode(new IDou('cxk', 2.5));
結(jié)果:
{"name":"cxk","year":2.5}
以上就是PHP中json浮點精度的解決方法的詳細內(nèi)容,更多關(guān)于PHP json浮點精度的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何用RabbitMQ和Swoole實現(xiàn)一個異步任務(wù)系統(tǒng)
從最開始的使用redis實現(xiàn)的單進程消費的異步任務(wù)系統(tǒng)到加入swoole的多進程消費模式,現(xiàn)在,我們的異步任務(wù)系統(tǒng)終于又能邁進一步。這回基于RabbitMQ的異步任務(wù)系統(tǒng)設(shè)計的的更加完善,包括多進程消費,異常重試等。2021-05-05如何使用php判斷所處服務(wù)器操作系統(tǒng)的類型
本篇文章是對如何使用php判斷所處服務(wù)器操作系統(tǒng)的類型進行了詳細的分析介紹,需要的朋友參考下2013-06-06PHP下通過exec獲得計算機的唯一標識[CPU,網(wǎng)卡 MAC地址]
PHP下通過exec獲得計算機的唯一標識的代碼,可獲得CPU,網(wǎng)卡 MAC地址信息。2011-06-06