在 PHP 中正确地对 FLOAT 数进行算术运算

Posted

技术标签:

【中文标题】在 PHP 中正确地对 FLOAT 数进行算术运算【英文标题】:Do arithmetic operations on FLOAT numbers correctly in PHP 【发布时间】:2018-12-10 23:06:37 【问题描述】:

我想要最简单的计算:

$example = 90000000000009132 - 1;
echo sprintf('%.0f',  $example);

令我惊讶的是:

           90000000000009136

我的问题是 - 我怎样才能在php中得到正确答案这样的操作? 你能给我一个正确的代码吗?

附言在网上搜索后,我找到了解释,说“这不是一个错误”以及一些关于float的神话等等......好吧,我不会开始证明为什么我认为这真的是是一个明显的错误,我们只是不在乎在 PC 的背景下发生了什么,但显而易见的是,当我们减去 X 时,我们没有得到正确的数字Y,所以这绝对是错误的! (但请不要在这方面与我争论,因为我的问题不同 - 上面写得很清楚)。

p.s.在被这个问题 anomaly donwvoted 之后,我在引用的链接中找不到真正的解决方案。他们都没有工作,至少在我的主机上。因此,大多数声称是解决方案的“答案”在某些主机上似乎毫无用处。

【问题讨论】:

为了尊重您的声誉,我谦虚地建议bcsub()。这么长时间以来您一直无法解决这个问题的确切原因有点令人担忧,尤其是如果您过去需要进行精确计算。 It's not an really obvious bug echo php_INT_MAX; 如果在减1之前打印会发生什么?如果这也给出了与 90000000000009132 不同的(更高的)数字,则可以缩小问题范围并简化问题。 浮点数不能代表所有可能的数字,而只能代表其中的 2^64 个。 90000000000009131 不是其中之一。使用最接近的替代方案。代码需要使用不同的类型。 【参考方案1】:

当然,PHP 浮点精度问题经常是一个关于 SO 的话题,但我仍然不明白为什么这个问题尽管有 18 票反对,因为这个问题非常精确地针对具体浮点数的具体问题提出。我已经解决了这个问题,每个人都应该清楚地理解这里的问题是什么(不仅仅是那些不精确的推理)。

我们开始 - 查看以下结果:

$example = 90000000000009132 - 1;

var_dump(PHP_INT_MAX);
var_dump($example); 
var_dump(PHP_FLOAT_MAX);
var_dump(sprintf('%.0g',  $example)); 
var_dump(sprintf('%.0f',  $example));

结果:

int(9223372036854775807) 
int(90000000000009131) 
float(1.7976931348623E+308) 
string(7) "9.0e+16" 
string(17) "90000000000009136"

意思是:

您没有超出最大整数大小,这将使 算术运算失败 你的计算结果是正确的整数 您未超过 PHP_FLOAT_MAX(需要 PHP 7.2),但其大小取决于您的环境 计算的浮点表示意外错误(因为您的 printf 语句先转换为浮点数,然后再转换为字符串)

那么为什么我们会得到这个奇怪的结果呢?让我们做一些测试:

var_dump(sprintf('%.0f',  90000000000009051));
var_dump(sprintf('%.0f',  90000000000009101));
var_dump(sprintf('%.0f',  90000000000009105));
var_dump(sprintf('%.0f',  90000000000009114));
var_dump(sprintf('%.0f',  90000000000009121));
var_dump(sprintf('%.0f',  90000000000009124));
var_dump(sprintf('%.0f',  90000000000009128));
var_dump(sprintf('%.0f',  90000000000009130));
var_dump(sprintf('%.0f',  90000000000009131));
var_dump(sprintf('%.0f',  90000000000009138));
var_dump(sprintf('%.0f',  90000000000009142));
var_dump(sprintf('%.0f',  90000000000009177));

这样的结果是:

string(17) "90000000000009056"
string(17) "90000000000009104"
string(17) "90000000000009104"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009120"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009136"
string(17) "90000000000009184"

看看这些数字。末尾的数字总是可以被 16 整除。信息技术中的 16 是什么意思?一个字节。十六进制。它告诉我们什么? 您可能已经超出了浮点数基本精确的限制(不一定在算术运算中)。

如果我们将这些数字减少一个零到 16 位呢?

var_dump(sprintf('%.0f',  9000000000009051));
var_dump(sprintf('%.0f',  9000000000009101));
var_dump(sprintf('%.0f',  9000000000009124));
var_dump(sprintf('%.0f',  9000000000009138));
var_dump(sprintf('%.0f',  9000000000009142));
var_dump(sprintf('%.0f',  9000000000009177));

...

string(16) "9000000000009051"
string(16) "9000000000009101"
string(16) "9000000000009124"
string(16) "9000000000009138"
string(16) "9000000000009142"
string(16) "9000000000009177"

输出总是正确的。好吧。如果我们把它提高一个零位数呢?

var_dump(sprintf('%.0f',  900000000000009051));
var_dump(sprintf('%.0f',  900000000000009101));
var_dump(sprintf('%.0f',  900000000000009124));
var_dump(sprintf('%.0f',  900000000000009138));
var_dump(sprintf('%.0f',  900000000000009142));
var_dump(sprintf('%.0f',  900000000000009177));

...

string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009088"
string(18) "900000000000009216"

有趣的是,这又是 16 的因数 - 更加不精确。

现在让我们来解开这个谜吧:

var_dump(strlen(sprintf('%.0f',  9000000000009051)));

猜猜结果是什么:

int(16)

哇。这意味着浮点数在 PHP 中基本上可以精确到 16 位(如果超过这个神奇的数字,它就会变得不精确)。

那么你的计算发生了什么?

由于您超过了 16 位的位数,所以最后四舍五入到下一个 16 位。

有关 PHP 中浮点精度的更多信息可以在 *** 上的几十个问题中找到,当然也可以在 PHP 手册中找到。

回答如何使您的代码正常工作:

如果您希望您的数字准确,请不要转换为浮点数。以整数为例,应该可以进行更高的计算。

如果超过 16 位数,像 -1 或 +1 这样的简单算术也会失败(至少在当前版本的 PHP 中看起来如此)。

【讨论】:

哇,很好的答案,谢谢!最后出现了一个相当大的用户,他可以很好地理解另一个程序员的问题。 (顺便说一句,更令人惊讶的是,投反对票的人更多,但最近似乎其他人投了一点票) 我也投了赞成票。我根本不明白为什么这是一个糟糕的问题,只是因为已经有关于 PHP 中浮点不精确性的不同问题(在这种情况下它会是重复的)。我最近与另一位 SO 重度用户交谈,他确信尤其是 SO 上的 PHP 社区很尴尬,例如 Python 或 Ruby 问题的处理要友好得多。 好吧,我不得不同意,发生了一些奇怪的事情。在我最后的 5-10 个问题中,我只是对非常合理的问题投了反对票。但是,许多其他受人尊敬的 SO 用户已经将这些用户称为“downvote 旅”:) 顺便说一句,你有非常完整的答案。我认为这甚至应该是其他主题的参考。

以上是关于在 PHP 中正确地对 FLOAT 数进行算术运算的主要内容,如果未能解决你的问题,请参考以下文章

0202 算术运算符

PHP 运算符

PHP运算符优先级(摘自在线工具)

float128 和 double-double 算术

浅析BigDecimal的使用

JavaScrpt算术运算符