『回复列表(17|隐藏机器人聊天)』
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可能不同。
你的代码大部分情况下没出问题,只是因为误差大小没有达到默认精度阙值。一但误差大小达到阙值,浮点数的默认字符串转换结果就会变成例子中的那样(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字符串转换精度阙值。
对于定点模拟(用整数来表示定点数),要记得最后格式化的时候不能转换为浮点数(也就是*100并强制转换为整数后不能/100变回去),而是应该使用字符串处理函数添加小数点。否则在变成浮点数后,再转换成整数时依然可以产生误差。
或者,最好的方法是从接收参数开始就转换成整数(用字符串处理函数去掉小数点),全程不要有小数,最后运算完了再通过字符串处理函数添加小数点,这样就不会产生误差。
备注:下文中 ^
表示乘方运算,不是异或运算。
decimal(14, 2)
)。也可以使用整数存储金额,把单位设置为分。*100
,这可能会导致数据从字符串转换为浮点数然后*100
,导致精度问题。建议使用sql语句*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
形式精确表示的数,没有精度损失。y = 0b1<x> * 2^n
形式表示,所以用了特殊方法表示的数,没有精度损失。y = 0b1<x> * 2^n
形式表示,但尾数的位数不够多,或者尾数出现“无限循环”的情况,此时出现精度损失。所以,以下行为是精度安全的:
-2^($尾数位数+1)-1
到2^($尾数位数+1)-1
内就可以(+1是因为隐含位)。在这种情况下,整数的最高位1被去掉,剩下的部分填充尾数(高位对齐),然后指数的值是整数的二进制位数。(0则是非规格化的,也是精确的。)以下行为不是精度安全的:
上面说到最好只使用整数运算,那么怎么用整数计算打折呢?
比如75折:
y = x * 75 / 100
只要乘法不溢出,结果就是良好的。在大部分编程语言中结果都还是整数。如果不是整数,舍入到整数即可。