欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

PHP中json浮點(diǎn)精度的解決方法

 更新時(shí)間:2024年10月22日 09:43:09   作者:cy_b  
這篇文章主要帶大家一起深入研究下PHP的json中,浮點(diǎn)型的精度該如何保留的問(wèn)題,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,希望對(duì)大家有所幫助

前言

之前開發(fā)的接口需要用到j(luò)son加簽,有一次對(duì)接JAVA時(shí),簽名怎么都過(guò)不了,仔細(xì)對(duì)比了字符串,發(fā)現(xiàn)是PHP進(jìn)行json_encode時(shí),會(huì)將浮點(diǎn)型所有無(wú)意義的0給去掉(echo和var_dump也會(huì)),而JAVA那邊沒有。遂在文檔中寫下: “json中請(qǐng)把無(wú)意義的0去掉”。 

最近又遇到這個(gè)事情,需求直接要求:顯示字符型,且精度要保留兩位小數(shù),于是不得不開始研究PHP的json中,浮點(diǎn)型的精度該如何保留的問(wèn)題。

解決方案

json_encode常量參數(shù)(無(wú)法解決)

相關(guān)知識(shí)

json_encode的函數(shù)原型如下:

json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false

眾所周知,json_encode的第一個(gè)進(jìn)階用法,就是它的第二個(gè)參數(shù)flags,也就是“可選的json編碼方式”,各種奇妙的常量。比如我最長(zhǎng)用到的,JSON_UNESCAPED_UNICODE,讓json不自動(dòng)進(jìn)行unicode轉(zhuǎn)換,直接輸出中文。所以第一個(gè)想到的,就是查看有沒有對(duì)應(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,恰好對(duì)應(yīng)的就是256,二進(jìn)制的設(shè)計(jì)是為了他們可以方便的復(fù)合使用。寫法也很多變,比如json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),json_encode($data, JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES),json_encode($data, 256 + 64)。都是一樣的實(shí)現(xiàn)。

PHP json_encode中文文檔

PHP json_encode常量文檔

其中和數(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)

做排列組合試驗(yàn)

$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ù)字,無(wú)意義的0仍舊會(huì)被處理掉

JSON_PRESERVE_ZERO_FRACTION的表現(xiàn)形式就有些奇怪,只能在有第一位小數(shù)且為0時(shí),只保留一位0

顯然,flags是無(wú)法滿足需求的。

配置項(xiàng)"serialize_precision"("precision")(無(wú)法解決)

文檔中有這么一句話

如果參數(shù)是 array 或 object,則會(huì)遞歸序列化。

編碼受傳入的 flags 參數(shù)影響,此外浮點(diǎn)值的編碼依賴于 serialize_precision。

serialize_precision文檔位置

serialize_precision int 序列化浮點(diǎn)數(shù)時(shí)存儲(chǔ)的有效數(shù)字的位數(shù)。-1 表示將使用增強(qiáng)算法來(lái)四舍五入此類數(shù)字。

PHP中,serialize_precision配置項(xiàng)用于序列化時(shí)控制浮點(diǎn)數(shù)的精度,而precision用于平常顯示時(shí)的控制。

我們?nèi)∫粋€(gè)數(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

可以比較清楚的看出這個(gè)配置的效果了,而且顯然,無(wú)法達(dá)成需求。

題外話:

測(cè)試時(shí)發(fā)現(xiàn),在PHP7.1以上的版本中,如果將serialize_precision的數(shù)值設(shè)置為很大,比如5.*版本默認(rèn)的17,得到的結(jié)果是: 17.199999999999999。precision同理,作用于echo,var_dump,print_r等。

所以建議日常使用,設(shè)置為默認(rèn)的-1就好。

字符串處理-正則

如此來(lái)看,從編碼配置層面似乎無(wú)法解決這個(gè)需求了,那么就使用最簡(jiǎn)單直接的辦法: 用正則,直接對(duì)字符串下手。

foreach ($data as &$item) {
    if (is_numeric($item)) {
        $item = sprintf("%.2f", $item);
    }
}
$json = json_encode($data);
// 浮點(diǎn)型轉(zhuǎn)換為數(shù)值型
$pattern = '/"(\d+\.\d+)"/';
$replacement = '$1';
$new_json = preg_replace($pattern, $replacement, $json);

這段函數(shù),是把數(shù)值全部先轉(zhuǎn)換為保留2位小數(shù)的字符串,進(jìn)行json_encode后,再把字符串中所有帶".",左右是數(shù)字的,外層的雙引號(hào)去掉。

如果你的json更為復(fù)雜,需要對(duì)正則進(jìn)行調(diào)整。

原理-PHP中浮點(diǎn)型的顯示

我們來(lái)看這么一段代碼,猜測(cè)下他的輸出結(jié)果會(huì)是什么:

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)

那么,為什么會(huì)出現(xiàn)float(1),1.00 === 1.0這樣奇怪的輸出呢?原因在于PHP內(nèi)核中變量容器Zval(Zend value)的實(shí)現(xiàn),以及顯示處理。

PHP是一個(gè)弱類型語(yǔ)言,一個(gè)變量,可以是任何類型,這也得益于Zval的實(shí)現(xiàn)。Zval,也就是_zval_struct這個(gè)結(jié)構(gòu)體,主要記錄了三塊東西:值,類型,引用計(jì)數(shù)。并沒有“顯示精度”這種屬性和配置。(引用計(jì)數(shù)和垃圾回收有關(guān))

所以在var_dump時(shí),顯示的是變量的類型float,以及和存儲(chǔ)的值,最近似的有意義的數(shù)值,也就是float(1)。而使用===對(duì)比時(shí),存儲(chǔ)的值相等,類型也相等,自然就會(huì)顯示成true。

對(duì)應(yīng)源碼

a) 對(duì)浮點(diǎn)型的輸出函數(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 是在這里進(jìn)行的影響,會(huì)在最終判斷是否整形,并加".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,就是從這里進(jìn)行的引入。

c) smart_str_append_double 對(duì)浮點(diǎn)型字符串的處理函數(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的寫法,清除無(wú)意義的0,在這里被實(shí)現(xiàn)。

如何顯示精度

如果要顯示確切的精度,只能轉(zhuǎn)換為字符串類型,有兩種方法:

$number = 1;
echo sprintf("%.2f", $number);
echo number_format($number, 2, '.', '');

兩種方法都在PHP4的版本實(shí)裝,可以放心使用。

需要注意的是,如果本身的位數(shù)超過(guò)精度,這兩種方法都會(huì)四舍五入。

另外,number_format的第三個(gè)參數(shù)為“小數(shù)點(diǎn)符號(hào)”,第四個(gè)參數(shù)為“千位分隔符”。默認(rèn)分別是"."和","。尤其是需要進(jìn)行數(shù)字計(jì)算和正常顯示時(shí),需要注意“千位分隔符”的設(shè)置。

關(guān)于"double"和"float"

PHP中的浮點(diǎn)型,是使用c中的double型實(shí)現(xiàn)的,全部都是遵循 IEEE754 標(biāo)準(zhǔn),64位的雙精度浮點(diǎn)數(shù),不存在單精度。

在PHP中,double和float的命名使用的很混亂。在源碼中,多見double,類型判斷用的也是IS_DOUBLE。但在7以后,顯示定義的類型,必須使用float。比如 function(float $num): float,這似乎是為了與其他語(yǔ)言的命名方式保持一致。

獲取類型的相關(guān)函數(shù),使用不同版本進(jìn)行了簡(jiǎn)單測(cè)試,很奇怪,盡量別用8.2:

gettype(1.0); // double
var_dump(1.0); // 8.2版本顯示為double,8.3及其他版本都是float,同時(shí)8.2版本也多出了文件位置的輸出

其他函數(shù):

// 都只是別名,功能一致
is_float();
is_double();
 
floatval();
doubleval();
 
...

浮點(diǎn)型的對(duì)比和精確計(jì)算

PHP float型文檔

從float文檔中可以看到,由于精度問(wèn)題,官方是不支持把浮點(diǎn)型進(jìn)行直接對(duì)比和計(jì)算的,“永遠(yuǎn)不要相信浮點(diǎn)數(shù)結(jié)果精確到了最后一位,也永遠(yuǎn)不要比較兩個(gè)浮點(diǎn)數(shù)是否相等”。(例如,0.1 + 0.2 在計(jì)算機(jī)中并不等于 0.3,而是等于 0.30000000000000004‌)

一般正常的四則運(yùn)算其實(shí)影響不大,但如果對(duì)精度有很高的要求,推薦使用BC系列函數(shù),或者GMP函數(shù)。

對(duì)比前,先使用round()函數(shù),將浮點(diǎn)型進(jìn)行四舍五入處理。(和官方給的處理方式類似,但更好理解)

$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以后,支持對(duì)浮點(diǎn)型添加下劃線,只是增加可讀性,和千分符類似:

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 及以上版本可用。

對(duì)象的序列化處理-JsonSerializable(json_encode的其他進(jìn)階用法)

閱讀json_encode文檔時(shí),還可以發(fā)現(xiàn),

JsonSerializable 文檔位置

實(shí)現(xiàn) JsonSerializable 的類可以 在 json_encode() 時(shí)定制他們的 JSON 表示法(序列化)。

go的json序列化比較常見,可以結(jié)合理解。

JAVA也有同名JsonSerializable方法,是將類信息也帶入json中,可以實(shí)現(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浮點(diǎn)精度的解決方法的詳細(xì)內(nèi)容,更多關(guān)于PHP json浮點(diǎn)精度的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論