PHP的intval()和floor()函数返回值太低的问题?

12

因为PHP中的浮点数类型不准确,而MySQL中的FLOAT比INT占用更多的空间(并且不准确),所以我总是将价格存储为INT,存储之前乘以100以确保我们有精确的2位小数。但是我认为PHP表现不佳。示例代码:

echo "<pre>";

$price = "1.15";
echo "Price = ";
var_dump($price);

$price_corrected = $price*100;
echo "Corrected price = ";
var_dump($price_corrected);


$price_int = intval(floor($price_corrected));
echo "Integer price = ";
var_dump($price_int);

echo "</pre>";

生成的输出:

Price = string(4) "1.15"
Corrected price = float(115)
Integer price = int(114)

我很惊讶。最终结果比预期低了1,我原本希望我的测试输出更像:

Price = string(4) "1.15"
Corrected price = float(114.999999999)
Integer price = int(114)

这将展示浮点数类型的不准确性。但是为什么floor(115)返回114??


这与PHP实际上没有什么关系,更多的是与所有计算机处理浮点数据的方式有关。如果您以前没有遇到过浮点不准确性,您可能需要阅读一下相关主题。 - jmucchiello
@jmucchiello,我不同意。我确实理解计算机如何处理浮点数据,这就是为什么我使用整数数据而不是浮点数据的原因。这是一个 PHP 的问题,因为 PHP 在底层数据明显是 114.99999999... 时显示为 115。 - Josh
5个回答

27

尝试使用以下方法作为快速修复:

$price_int = intval(floor($price_corrected + 0.5));
你遇到的问题并不是PHP的错,所有使用浮点运算的编程语言都会有类似的问题。
货币计算的一般准则是永远不要使用浮点数(无论是在数据库中还是在脚本中)。通过始终存储分而不是元,您可以避免各种问题。分是整数,您可以自由地将它们相加,并乘以其他整数。每当您显示数字时,请确保在最后两个数字前插入一个句点。
你得到114而不是115的原因是floor向下舍入,靠近最近的整数,因此floor(114.999999999)变成了114。更有趣的问题是为什么1.15 * 100是114.999999999而不是115。这样做的原因是1.15不完全等于115/100,但稍微小一点,因此如果乘以100,则得到比115略小的数字。
这里是更详细的解释,说明echo 1.15 * 100;的操作:
- 它将1.15解析为二进制浮点数。这涉及四舍五入,它碰巧向下舍入了一点,以获得最接近1.15的二进制浮点数。之所以不能得到精确的数字(没有舍入误差)的原因是1.15在2进制中具有无限数量的数字。 - 它将100解析为二进制浮点数。这涉及四舍五入,但由于100是一个小整数,因此舍入误差为零。 - 它计算前两个数字的乘积。这也涉及一些舍入,以找到最接近的二进制浮点数。在此操作中,舍入误差碰巧为零。 - 它将二进制浮点数转换为带有句点的十进制数,并打印此表示形式。这也涉及一些舍入。
PHP打印出令人惊讶的Corrected price = float(115)(而不是114.999...)的原因是var_dump不会打印精确的数字!,但它会打印数字舍入到n-2(或n-1)位,其中n位数字是计算的精度。您可以轻松验证此操作。
echo 1.15 * 100;  # this prints 115
printf("%.30f", 1.15 * 100);  # you 114.999....
echo 1.15 * 100 == 115.0 ? "same" : "different";  # this prints `different'
echo 1.15 * 100 < 115.0 ? "less" : "not-less";    # this prints `less'

如果你要打印浮点数,请记住:在打印浮点数时,你不总是能看到所有的数字

另请参阅PHP 浮点数文档开头的大警告。


$price_int = intval(floor($price_corrected + 0.5)) 等同于 $price_int = intval(round($price_corrected)). - chaos
@pts,chaos是正确的,实际上我的原始代码就是这样的。没有区别。我的问题是,为什么PHP $price_converted是115,而实际上它是114.999999?(我确实明白为什么是114.99999...) - Josh
@pts,我之前说混沌是正确的时候错了,我误读了你的代码。那个可能有效。 - Josh
好的,这个确实可以工作。但是哪个更好,+0.5 还是使用 round() 函数?使用 round() 有什么影响? - Josh
使用 round(),因为它是自包含且更加清晰的。+0.5 可能会引入更多的舍入。 - pts
PHP打印“Corrected price = float(115)”的原因是var_dump不会打印精确的数字,而是打印四舍五入到n-2(或n-1)位小数的数字,其中n位小数是计算的精度。 - pts

4
也许这是另一个可能的解决方案:“
intval(number_format($problematic_float, 0, '', ''));

您,是的,就是您,先生,应该得到一个赞。刚刚遇到一个奇怪的错误,然后解决了它。我只是在 number_format() 调用中添加了空字符串,以使结果数字成为一个没有逗号和点的整数。 - Tomasz Kowalczyk
我遇到了intval(16.74 * 100)返回1673的问题,而被接受的答案并没有帮助到我。 - Billy

4
其他答案已经涵盖了问题的原因和一个不错的解决方法,我相信。
从不同的角度来解决问题:
如果要在MySQL中存储价格值,您应该考虑使用DECIMAL类型,它可以让您存储带有小数位的精确值。请参考DECIMAL类型

+1,但我想补充的是,我不确定mySQL中的FLOAT类型有多“不准确”。它比执行任何额外的乘法操作要不准确得多,并且使您的数据处于更好的分析状态。无论如何,他说的没错。 - cgp
@Chad Birch,INT 使用的空间比 DECIMAL 少得多,是吗?我一直觉得 INT + 逻辑(用于知道要偏移多少小数位)比 DECIMAL 更高效。特别是对于价格这样的东西。实际上,该列是 price_in_cents。 - Josh
1
不,除非你的DECIMAL有很多位数。根据手册,INT使用4个字节(假设你使用的是实际的INT而不是SMALLINT或其他类型),DECIMAL存储方式各不相同:“每九位数字的倍数需要四个字节,而“剩下的”数字则需要四个字节的某个分数”。完整信息请参阅:http://dev.mysql.com/doc/refman/5.1/en/storage-requirements.html - Chad Birch
这非常有帮助。出于某种原因,我认为DECIMAL实际上只是一个CHAR()列……也许那是在旧版本的MySQL中?也许我只是疯了…… :-) - Josh

3

PHP基于有效数字进行四舍五入,它会隐藏不准确性(在第2行)。当使用floor时,它并不知道更好的方法,因此将所有数值都向下取整。


@altCognito,你知道为什么它在第2行隐藏了不准确性吗?我觉得这才是我的问题所在。 - Josh
1
我相信这与归一化有关,答案在这里:http://en.wikipedia.org/wiki/Floating_point#Multiplication :| - cgp

0

如上所述,这并不是 PHP 本身的问题,而是处理无法表示为有限浮点值的分数时出现精度丢失的问题。

解决方案是确保在处理浮点值并需要保持精度时,使用 gmp 函数或 BC 数学函数 - bcpow、bcmul 等等,问题将很容易得到解决。

例如,不要使用 $price_corrected = $price*100;,而应该使用 $price_corrected = bcmul($price,100);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接