it编程 > 编程语言 > Php

PHP中json浮点精度的解决方法

140人参与 2024-11-25 Php

前言

之前开发的接口需要用到json加签,有一次对接java时,签名怎么都过不了,仔细对比了字符串,发现是php进行json_encode时,会将浮点型所有无意义的0给去掉(echo和var_dump也会),而java那边没有。遂在文档中写下: “json中请把无意义的0去掉”。 

最近又遇到这个事情,需求直接要求:显示字符型,且精度要保留两位小数,于是不得不开始研究php的json中,浮点型的精度该如何保留的问题。

解决方案

json_encode常量参数(无法解决)

相关知识

json_encode的函数原型如下:

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

众所周知,json_encode的第一个进阶用法,就是它的第二个参数flags,也就是“可选的json编码方式”,各种奇妙的常量。比如我最长用到的,json_unescaped_unicode,让json不自动进行unicode转换,直接输出中文。所以第一个想到的,就是查看有没有对应的常量参数。

查看源码,json的常量参数都放在 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,恰好对应的就是256,二进制的设计是为了他们可以方便的复合使用。写法也很多变,比如json_encode($data, json_unescaped_unicode | json_unescaped_slashes),json_encode($data, json_unescaped_unicode + json_unescaped_slashes),json_encode($data, 256 + 64)。都是一样的实现。

php json_encode中文文档

php json_encode常量文档

其中和数字有关的,就是php_json_numeric_check,以及php_json_preserve_zero_fraction

// 将所有数字字符串编码成数字(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;

结果

{"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}

结论

可以看到json_numeric_check正如文档描述中的那样,将所有数字字符串都编码成了数字,无意义的0仍旧会被处理掉

json_preserve_zero_fraction的表现形式就有些奇怪,只能在有第一位小数且为0时,只保留一位0

显然,flags是无法满足需求的。

配置项"serialize_precision"("precision")(无法解决)

文档中有这么一句话

如果参数是 array 或 object,则会递归序列化。

编码受传入的 flags 参数影响,此外浮点值的编码依赖于 serialize_precision

serialize_precision文档位置

serialize_precision int 序列化浮点数时存储的有效数字的位数。-1 表示将使用增强算法来四舍五入此类数字。

php中,serialize_precision配置项用于序列化时控制浮点数的精度,而precision用于平常显示时的控制。

我们取一个数字,echo json_encode(17.2);,将serialize_precision,从低到高设置。得到下面的结果:

0   2.0e+1
1   2.0e+1
2   17
3   17.2
4   17.2

可以比较清楚的看出这个配置的效果了,而且显然,无法达成需求。

题外话:

测试时发现,在php7.1以上的版本中,如果将serialize_precision的数值设置为很大,比如5.*版本默认的17,得到的结果是: 17.199999999999999precision同理,作用于echo,var_dump,print_r等。

所以建议日常使用,设置为默认的-1就好。

字符串处理-正则

如此来看,从编码配置层面似乎无法解决这个需求了,那么就使用最简单直接的办法: 用正则,直接对字符串下手。

foreach ($data as &$item) {
    if (is_numeric($item)) {
        $item = sprintf("%.2f", $item);
    }
}
$json = json_encode($data);
// 浮点型转换为数值型
$pattern = '/"(\d+\.\d+)"/';
$replacement = '$1';
$new_json = preg_replace($pattern, $replacement, $json);

这段函数,是把数值全部先转换为保留2位小数的字符串,进行json_encode后,再把字符串中所有带".",左右是数字的,外层的双引号去掉。

如果你的json更为复杂,需要对正则进行调整。

原理-php中浮点型的显示

我们来看这么一段代码,猜测下他的输出结果会是什么:

echo 1.0;
var_dump(1);
var_dump(1.0);
var_dump(1.0 === 1);
var_dump(1.00 === 1.0);

结果:

1
int(1)
float(1)
bool(false)
bool(true)

那么,为什么会出现float(1),1.00 === 1.0这样奇怪的输出呢?原因在于php内核中变量容器zval(zend value)的实现,以及显示处理。

php是一个弱类型语言,一个变量,可以是任何类型,这也得益于zval的实现。zval,也就是_zval_struct这个结构体,主要记录了三块东西:值,类型,引用计数。并没有“显示精度”这种属性和配置。(引用计数和垃圾回收有关)

所以在var_dump时,显示的是变量的类型float,以及和存储的值,最近似的有意义的数值,也就是float(1)。而使用===对比时,存储的值相等,类型也相等,自然就会显示成true。

对应源码

a) 对浮点型的输出函数 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 对浮点型字符串的处理函数: 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,在这里被实现。

如何显示精度

如果要显示确切的精度,只能转换为字符串类型,有两种方法:

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

两种方法都在php4的版本实装,可以放心使用。

需要注意的是,如果本身的位数超过精度,这两种方法都会四舍五入

另外,number_format的第三个参数为“小数点符号”,第四个参数为“千位分隔符”。默认分别是"."和","。尤其是需要进行数字计算和正常显示时,需要注意“千位分隔符”的设置。

关于"double"和"float"

php中的浮点型,是使用c中的double型实现的,全部都是遵循 ieee754 标准,64位的双精度浮点数,不存在单精度。

在php中,double和float的命名使用的很混乱。在源码中,多见double,类型判断用的也是is_double。但在7以后,显示定义的类型,必须使用float。比如 function(float $num): float,这似乎是为了与其他语言的命名方式保持一致。

获取类型的相关函数,使用不同版本进行了简单测试,很奇怪,尽量别用8.2:

gettype(1.0); // double
var_dump(1.0); // 8.2版本显示为double,8.3及其他版本都是float,同时8.2版本也多出了文件位置的输出

其他函数:

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

浮点型的对比和精确计算

php float型文档

从float文档中可以看到,由于精度问题,官方是不支持把浮点型进行直接对比和计算的,“永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等”。(例如,0.1 + 0.2 在计算机中并不等于 0.3,而是等于 0.30000000000000004‌)

一般正常的四则运算其实影响不大,但如果对精度有很高的要求,推荐使用bc系列函数,或者gmp函数

对比前,先使用round()函数,将浮点型进行四舍五入处理。(和官方给的处理方式类似,但更好理解)

$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常量参数版本适用性

对象的序列化处理-jsonserializable(json_encode的其他进阶用法)

阅读json_encode文档时,还可以发现,

jsonserializable 文档位置

实现 jsonserializable 的类可以 在 json_encode() 时定制他们的 json 表示法(序列化)。

go的json序列化比较常见,可以结合理解。

java也有同名jsonserializable方法,是将类信息也带入json中,可以实现反序列化,不常用。

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));

结果:

{"name":"cxk","year":2.5}

以上就是php中json浮点精度的解决方法的详细内容,更多关于php json浮点精度的资料请关注代码网其它相关文章!

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

PHP实现页面跳转的多种方法

11-25

phpStudy在运行PHP文件时出现中文乱码的有效解决方法

11-25

PHP如何抛出和接收错误的详细指南

11-25

PHP中防SQL注入的主要方法

11-25

基于PHP实现密码管理工具

11-25

PHP防止Shell命令注入的有效方法

11-25

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论