你先打印$actual_money,看看精度有没有丢失。如果已经是丢失了,就用高精度的数学运算函数来做运算,而不是简单的A + B 小米5s Plus
0.01无法用浮点数精确表示,每次累加结果都比你预期的多一点或者少一点,直到6次之后超出了你PHP字符串转换的精度阙值表现了出来。 看这个例子:
<?php for ($d=0.0; $d < 0.1; $d +=0.01) { var_dump(sprintf('%0.32f',$d)); }
string(34) "0.00000000000000000000000000000000" string(34) "0.01000000000000000020816681711722" string(34) "0.02000000000000000041633363423443" string(34) "0.02999999999999999888977697537484" string(34) "0.04000000000000000083266726846887" string(34) "0.05000000000000000277555756156289" string(34) "0.06000000000000000471844785465692" string(34) "0.07000000000000000666133814775094" string(34) "0.08000000000000000166533453693773" string(34) "0.08999999999999999666933092612453" string(34) "0.09999999999999999167332731531133"
@胡椒舰长,不建议使用浮点数表示需要精确表示的数据类型。可以考虑使用整数表示法(*100)或者定点数函数库。 如果一定要使用浮点数,应该使用sprintf('%0.2f', $d)转换为字符串。PHP的(string)操作默认精度应该是sprintf('%0.14f', $d),不同版本的PHP可能不同。
sprintf('%0.2f', $d)
(string)
sprintf('%0.14f', $d)
你的代码大部分情况下没出问题,只是因为误差大小没有达到默认精度阙值。一但误差大小达到阙值,浮点数的默认字符串转换结果就会变成例子中的那样(0.0600000000000000047等),导致与数据库中的定点数不匹配。如果不自己控制精度(比如sprintf('%0.2f',$d)),出问题是迟早的事。
0.0600000000000000047
sprintf('%0.2f',$d)
浮点数累加后精度误差会扩大。以下例子展示了需要多久后才能产生默认设置中可见的差异:
<?php for ($d=0.0; true; $d +=0.01) { $a = (string)$d; $b = sprintf('%0.2f', $d); if ($a != $b) { var_dump($a, $b); break; } }
在我的PHP中,结果是:
string(15) "4.4299999999999" string(4) "4.43"
结果可能会因为PHP版本的不同而不同。
此外,楼主的代码还有一个偶然因素,$param['money']是多少并不确定。 所以,浮点数不仅可以无法精确表示0.01,也可以恰好无法精确表示$param['money'],并且误差较大。此时,两个数加起来的误差完全可以直接超过PHP字符串转换精度阙值。
$param['money']
0.01
对于定点模拟(用整数来表示定点数),要记得最后格式化的时候不能转换为浮点数(也就是*100并强制转换为整数后不能/100变回去),而是应该使用字符串处理函数添加小数点。否则在变成浮点数后,再转换成整数时依然可以产生误差。 或者,最好的方法是从接收参数开始就转换成整数(用字符串处理函数去掉小数点),全程不要有小数,最后运算完了再通过字符串处理函数添加小数点,这样就不会产生误差。
@胡椒舰长,考虑用字符串处理函数 $d = explode('.', $d); $d = (int)($d[0].str_pad(substr($d[1], 0, 2), 2, '0'));
最近项目中后端同学使用的是node.js也出现了精度问题导致验证支付失败,在掘金上刷文章又看到精度问题的文章,看QQ群的thinkphp周刊里又提到运算精度问题,逛葫芦林也看到类似文章 ...快入魔了
这是什么原因?@老虎会游泳,那么如果是电商场景,有一些优惠券之类的加减运算操作小数的,代码层以及数据库的最佳实践是什么? 小米5黑色低配版
备注:下文中 ^ 表示乘方运算,不是异或运算。
^
@水木易安,对于金融相关计算,以下是建议:
decimal(14, 2)
*100
// double的二进制表示: <1bit符号位><11bit指数><52bit尾数> $double的值 = bindec("1$尾数") * pow(2, $指数-1023) * ($符号位==1 ? -1 : 1);
尾数有一个隐含位1,也就是说它总是1开头,所以开头的1不包含在二进制表示里面。在浮点计算完成后,结果的尾数部分必须表示为1开头,也就是把尾数的前导0去掉,直到开头为1,同时缩小指数部分,然后删掉开头的1,这一过程叫做浮点数的“规格化”。
并不是所有浮点数都是“规格化”的,某些浮点数值是非规格化的,比如0(因为具有隐含位,底数的最小值为1,1的任意次方都不等于0,所以0没有办法规格化,必须用特殊方法表示)。
具体来说,浮点数的表示方法分为以下几种情况:
y = 0b1<x> * 2^n
所以,以下行为是精度安全的:
-2^($尾数位数+1)-1
2^($尾数位数+1)-1
以下行为不是精度安全的:
上面说到最好只使用整数运算,那么怎么用整数计算打折呢? 比如75折: y = x * 75 / 100 只要乘法不溢出,结果就是良好的。在大部分编程语言中结果都还是整数。如果不是整数,舍入到整数即可。
y = x * 75 / 100
@水木易安,哦对了,其实如果只是为了防止“相等”比较时出现不匹配的问题,只需要做以下两件事就可以了:
decimal(n, 2)
round($x, 2)
sprintf('%0.2f', $x)
@老虎会游泳,学习了 小米5黑色低配版
@iola1999,阙值和阈值长的那么像,你居然看得出来
你先打印$actual_money,看看精度有没有丢失。如果已经是丢失了,就用高精度的数学运算函数来做运算,而不是简单的A + B
小米5s Plus
0.01无法用浮点数精确表示,每次累加结果都比你预期的多一点或者少一点,直到6次之后超出了你PHP字符串转换的精度阙值表现了出来。
看这个例子:
@胡椒舰长,不建议使用浮点数表示需要精确表示的数据类型。可以考虑使用整数表示法(*100)或者定点数函数库。
如果一定要使用浮点数,应该使用
sprintf('%0.2f', $d)
转换为字符串。PHP的(string)
操作默认精度应该是sprintf('%0.14f', $d)
,不同版本的PHP可能不同。你的代码大部分情况下没出问题,只是因为误差大小没有达到默认精度阙值。一但误差大小达到阙值,浮点数的默认字符串转换结果就会变成例子中的那样(
0.0600000000000000047
等),导致与数据库中的定点数不匹配。如果不自己控制精度(比如sprintf('%0.2f',$d)
),出问题是迟早的事。浮点数累加后精度误差会扩大。以下例子展示了需要多久后才能产生默认设置中可见的差异:
在我的PHP中,结果是:
结果可能会因为PHP版本的不同而不同。
此外,楼主的代码还有一个偶然因素,
$param['money']
是多少并不确定。所以,浮点数不仅可以无法精确表示
0.01
,也可以恰好无法精确表示$param['money']
,并且误差较大。此时,两个数加起来的误差完全可以直接超过PHP字符串转换精度阙值。对于定点模拟(用整数来表示定点数),要记得最后格式化的时候不能转换为浮点数(也就是*100并强制转换为整数后不能/100变回去),而是应该使用字符串处理函数添加小数点。否则在变成浮点数后,再转换成整数时依然可以产生误差。
或者,最好的方法是从接收参数开始就转换成整数(用字符串处理函数去掉小数点),全程不要有小数,最后运算完了再通过字符串处理函数添加小数点,这样就不会产生误差。
如果用户输入的是 0.1而非0.10,那应该是(int)0.1*100,然后
转成String?
@胡椒舰长,考虑用字符串处理函数
$d = explode('.', $d);
$d = (int)($d[0].str_pad(substr($d[1], 0, 2), 2, '0'));
最近项目中后端同学使用的是node.js也出现了精度问题导致验证支付失败,在掘金上刷文章又看到精度问题的文章,看QQ群的thinkphp周刊里又提到运算精度问题,逛葫芦林也看到类似文章 ...快入魔了
这是什么原因?@老虎会游泳,那么如果是电商场景,有一些优惠券之类的加减运算操作小数的,代码层以及数据库的最佳实践是什么?
小米5黑色低配版
备注:下文中
^
表示乘方运算,不是异或运算。@水木易安,对于金融相关计算,以下是建议:
decimal(14, 2)
)。也可以使用整数存储金额,把单位设置为分。*100
,这可能会导致数据从字符串转换为浮点数然后*100
,导致精度问题。建议使用sql语句*100,或者拿到结果后用字符串操作进行转换(直接去掉小数点)。转换回定点数的时候也一样,添加小数点。尾数有一个隐含位1,也就是说它总是1开头,所以开头的1不包含在二进制表示里面。在浮点计算完成后,结果的尾数部分必须表示为1开头,也就是把尾数的前导0去掉,直到开头为1,同时缩小指数部分,然后删掉开头的1,这一过程叫做浮点数的“规格化”。
并不是所有浮点数都是“规格化”的,某些浮点数值是非规格化的,比如0(因为具有隐含位,底数的最小值为1,1的任意次方都不等于0,所以0没有办法规格化,必须用特殊方法表示)。
具体来说,浮点数的表示方法分为以下几种情况:
y = 0b1<x> * 2^n
形式精确表示的数,没有精度损失。y = 0b1<x> * 2^n
形式表示,所以用了特殊方法表示的数,没有精度损失。y = 0b1<x> * 2^n
形式表示,但尾数的位数不够多,或者尾数出现“无限循环”的情况,此时出现精度损失。所以,以下行为是精度安全的:
-2^($尾数位数+1)-1
到2^($尾数位数+1)-1
内就可以(+1是因为隐含位)。在这种情况下,整数的最高位1被去掉,剩下的部分填充尾数(高位对齐),然后指数的值是整数的二进制位数。(0则是非规格化的,也是精确的。)以下行为不是精度安全的:
上面说到最好只使用整数运算,那么怎么用整数计算打折呢?
比如75折:
y = x * 75 / 100
只要乘法不溢出,结果就是良好的。在大部分编程语言中结果都还是整数。如果不是整数,舍入到整数即可。
@水木易安,哦对了,其实如果只是为了防止“相等”比较时出现不匹配的问题,只需要做以下两件事就可以了:
decimal(n, 2)
。round($x, 2)
并且转换为字符串再发送给数据库。或者sprintf('%0.2f', $x)
@老虎会游泳,学习了
小米5黑色低配版
@iola1999,阙值和阈值长的那么像,你居然看得出来