30

我有一个非常奇怪的问题。如果我减去 2 个浮点变量,其中一个是数学运算的结果,我会得到一个错误的值。

例子:

var_dump($remaining);
var_dump($this->hours_sub['personal']);
echo $remaining-$this->hours_sub['personal'];

这是输出:

float 5.4
float 1.4
5.3290705182008E-15

5.4-1.4应该是4 如果我将这两个值相加,结果是正确的。

我的错误在哪里?它不能是一个四舍五入的问题。

4

4 回答 4

105

如果仍然有人在访问此页面时遇到类似的问题,其中浮点数减法会导致错误或奇怪的值。下面我将更详细地解释这个问题。

它与 PHP 没有直接关系,也不是错误。但是,每个程序员都应该意识到这个问题。

二十年前,这个问题甚至夺走了许多人​​的生命。

1991 年 2 月 25 日,MIM-104 爱国者导弹连中的一个不正确的浮点运算(称为舍入误差)阻止了它在沙特阿拉伯达兰拦截一枚飞毛腿导弹,造成 28 名士兵死亡,近 100 名美军第 14 师的士兵受伤。军需支队。

但为什么会发生呢?

原因是浮点值表示有限的精度。因此,在任何处理(截断)之后,值可能不会具有相同的字符串表示形式。它还包括在脚本中编写一个浮点值并直接打印它而无需任何数学运算。

只是一个简单的例子:

$a = '36';
$b = '-35.99';
echo ($a + $b);

你会期望它打印0.01,对吧?但它会打印一个非常奇怪的答案,比如0.009999999999998

与其他数字一样,浮点数 double 或 float 以 0 和 1 的字符串形式存储在内存中。浮点数与整数的不同之处在于我们在查看 0 和 1 时如何解释它们。它们的存储方式有很多标准。

浮点数通常作为符号位、指数字段和有效数或尾数从左到右打包到计算机数据中......

由于缺少足够的空间,十进制数不能很好地用二进制表示。所以,你不能1/3完全按原样表达0.3333333...,对吧?为什么我们不能表示0.01为二进制浮点数是出于同样的原因。1/1000.00000010100011110101110000.....带重复的10100011110101110000

如果以二进制0.01的简化和系统截断形式保存01000111101011100001010,当它被转换回十进制时,它的读取方式将0.0099999....取决于系统(64 位计算机将提供比 32 位更好的精度)。在这种情况下,操作系统决定是否按照它看到的方式打印它,或者如何以更易于阅读的方式进行打印。因此,他们想要如何表示它取决于机器。但是可以用不同的方法在语言级别上进行保护。

如果您使用格式化结果

echo number_format(0.009999999999998, 2);

它会打印0.01

这是因为在这种情况下,您指示应该如何阅读它以及您需要多高的精度。

注意 number_format() 不是唯一的函数,还有一些其他函数和方法可以用来告诉编程语言关于精度期望的信息。

参考文献:
https ://sdqweb.ipd.kit.edu/publications/pdfs/saglam2016a.pdf
https://en.wikipedia.org/wiki/Round-off_error

于 2014-12-18T04:26:02.123 回答
5

这对我有用:

<?php
    $a = 96.35;
    $b = 96.01;

    $c = ( ( floor($a * 100) - floor($b * 100) ) / 100 );

    echo $c; // should see 0.34 exactly instead of 0.33999999999999
?>

由于浮点减法运算出现问题,我决定通过将其转换为整数运算来消除它,然后再次将结果备份为浮点数。

我更喜欢这种解决方案,因为基本上它确实可以防止计算错误,而不是使用其他函数来汇总结果。

于 2018-11-14T13:31:04.190 回答
3

除了使用 number_format() 之外,还有其他三种方法可以获得正确的结果。一个涉及做一些数学运算,如下所示:

<?php

$a = '36';
$b = '-35.99';

$a *= 100;
$b *= 100;

echo (($a + $b)/100),"\n";

演示

或者,您可以简单地使用 printf():

<?php

$a = '36';
$b = '-35.99';

printf("\n%.2f",($a+$b));

演示

请注意,如果没有精度说明符, printf() 结果将包含尾随零小数,如下所示:0.010000

您还可以使用 BC 数学函数 bcadd(),如下所示:

<?php
$a = '36';
$b = '-35.99';
echo "\n",bcadd($a,$b,2);

演示

于 2017-04-29T22:04:13.690 回答
1

我写了一个简单的函数来处理这个问题。它的工作原理类似于 php 的 bcmath 扩展中的 bcadd 函数。您将 2 个字符串形式的十进制数字 $a 和 $b 传递给它,并指定应该使用多少个小数,这些小数必须与 $a 和 $b 中的小数位数相匹配。

如您所见,它将使用整数进行数学运算,然后转换回字符串,而无需在任何时候使用浮点运算。

function decimalAdd($a,$b,$numDecimals=2) {
    $intSum=(int)str_replace(".","",$a)+(int)str_replace(".","",$b);
    $paddedIntSum=str_pad(abs($intSum),$numDecimals,0,STR_PAD_LEFT);
    $result=($intSum<0?"-":"").($intSum<100&&$intSum>-100?"0":"").substr_replace($paddedIntSum,".",-$numDecimals,0);
    return $result;
}

示例用法:

echo decimalAdd("36.00","-35.99");

0.01

于 2020-04-30T09:29:49.937 回答