比较 php 中的 float

我想比较 PHP 中的两个浮点数,如下面的示例代码所示:

$a = 0.17;
$b = 1 - 0.83; //0.17
if($a == $b ){
echo 'a and b are same';
}
else {
echo 'a and b are not same';
}

在这段代码中,它返回的是 else条件的结果,而不是 if条件,尽管 $a$b是相同的。在 PHP 中有什么特殊的方法来处理/比较浮点数吗?

如果是,那么请帮助我解决这个问题。

还是我的服务器配置有问题?

128682 次浏览

如果你这样做,他们 应该是相同的。但是请注意,浮点值的一个特点是,看起来产生相同值的计算实际上不需要相同。因此,如果 $a是一个字面 .17,而 $b是通过计算到达这里的,那么它们很可能是不同的,尽管它们都显示相同的值。

通常你不会像这样比较浮点值的相等性,你需要使用一个最小的可接受的差异:

if (abs(($a-$b)/$b) < 0.00001) {
echo "same";
}

差不多吧。

首先阅读红色警告 在手册里。绝对不要比较浮点数是否相等。应该使用 ε 技术。

例如:

if (abs($a-$b) < PHP_FLOAT_EPSILON) { … }

其中 PHP_FLOAT_EPSILON是常数,表示一个非常小的数字(您必须在7.2之前的旧版本的 PHP 中定义它)

如果你只是这样写,它可能会工作,所以我想你已经简化了它的问题。(让问题保持简洁通常是一件非常好的事情。)

但在这种情况下,我想象一个结果是一个计算,一个结果是一个常数。

这违反了浮点编程的基本规则: 永远不要做等式比较

原因有点微妙,但重要的是要记住,它们通常不起作用(讽刺的是,除了整数值) ,另一种选择是模糊的比较,大致如下:

if abs(a - y) < epsilon



1.其中一个主要问题涉及到我们在程序中写数字的方式。我们把它们写成十进制字符串,因此我们写的大多数分数都没有精确的机器表示。它们没有精确的有限形式,因为它们以二进制形式重复。每个机器分数都是 x/2N形式的有理数。现在,这些常数是十进制的,每个十进制常数都是形式为 x/(2N * 5)的有理数。5的数字是奇数,所以它们中的任何一个都没有2N因子。只有当 m = = 0时,分数的二进制和十进制展开式才有有限表示。因此,1.25是精确的,因为它是5/(22 * 50) ,而0.1不是因为它是1/(20 * 51)。事实上,在1.01系列中。.1.99只有3个数字是精确可表示的: 1.25、1.50和1.75。

或者尝试使用 BC 数学函数:

<?php
$a = 0.17;
$b = 1 - 0.83; //0.17


echo "$a == $b (core comp oper): ", var_dump($a==$b);
echo "$a == $b (with bc func)  : ", var_dump( bccomp($a, $b, 3)==0 );

结果:

0.17 == 0.17 (core comp oper): bool(false)
0.17 == 0.17 (with bc func)  : bool(true)

下面是比较浮点数或十进制数的解决方案

//$fd['someVal'] = 2.9;
//$i for loop variable steps 0.1
if((string)$fd['someVal']== (string)$i)
{
//Equal
}

铸造一个 decimal变量到 string,你会很好。

如前所述,在 PHP 中执行浮点比较(无论是等于、大于还是小于)时要非常小心。然而,如果你只对一些有效数字感兴趣,你可以这样做:

$a = round(0.17, 2);
$b = round(1 - 0.83, 2); //0.17
if($a == $b ){
echo 'a and b are same';
}
else {
echo 'a and b are not same';
}

使用四舍五入到小数点后2位(或3或4)将导致预期的结果。

这在 PHP 5.3.27上对我很有用。

$payments_total = 123.45;
$order_total = 123.45;


if (round($payments_total, 2) != round($order_total, 2)) {
// they don't match
}

如果要将浮点值与等式进行比较,避免操作系统、语言、处理器等的 内部四舍五入策略风险的一个简单方法是比较这些值的 < em > 字符串表示

您可以使用以下任何一种方法来产生所需的结果: https://3v4l.org/rUrEq

字符串类型铸造

if ( (string) $a === (string) $b) { … }

字符串连接

if ('' . $a === '' . $b) { … }

Strval 函数

if (strval($a) === strval($b)) { … }

当涉及到检查相等性时,字符串表示远没有浮点数那么挑剔。

如果你有一个小的,有限的小数点,这将是可以接受的,下面的工作很好(虽然慢性能比 ε 解决方案) :

$a = 0.17;
$b = 1 - 0.83; //0.17


if (number_format($a, 3) == number_format($b, 3)) {
echo 'a and b are same';
} else {
echo 'a and b are not same';
}

比较浮点数是否相等有一个简单的 O (n)算法。

必须将每个 float 值转换为字符串,然后使用整数比较运算符比较从每个 float 的字符串表示形式左侧开始的每个数字。PHP 将在比较之前将每个索引位置中的数字自动转换为一个整数。第一个大于另一个的数字将中断循环,并将其所属的浮点数声明为两个数字中较大的一个。平均来说,会有1/2 * n 的比较。对于彼此相等的浮点数,有 n 个比较。这是算法的最坏情况。最好的情况是每个浮点数的第一个数字不同,只导致一个比较。

不能在原始浮点值上使用 INTEGER COMPARISON OPERATORS 以生成有用的结果。这些操作的结果没有任何意义,因为您不是在比较整数。您正在侵犯每个运算符的域,从而产生无意义的结果。这也适用于 delta 比较。

使用整数比较运算符的目的是: 比较整数。

简化解决方案:

<?php


function getRand(){
return ( ((float)mt_rand()) / ((float) mt_getrandmax()) );
}


$a = 10.0 * getRand();
$b = 10.0 * getRand();


settype($a,'string');
settype($b,'string');


for($idx = 0;$idx<strlen($a);$idx++){
if($a[$idx] > $b[$idx]){
echo "{$a} is greater than {$b}.<br>";
break;
}
else{
echo "{$b} is greater than {$a}.<br>";
break;
}
}


?>

最好使用 原生 PHP 比较:

bccomp($a, $b, 3)
// Third parameter - the optional scale parameter
// is used to set the number of digits after the decimal place
// which will be used in the comparison.

如果两个操作数相等,返回0; 如果 left _ operand 为 大于 right _ operand,否则为 -1。

对于 PHP 7.2,可以使用 PHP _ FLOAT _ EPSILON (http://php.net/manual/en/reserved.constants.php) :

if(abs($a-$b) < PHP_FLOAT_EPSILON){
echo 'a and b are same';
}

二零一九年

DR

使用我下面的函数,就像这个 if(cmpFloats($a, '==', $b)) { ... }

  • 易读/易写/易更改: cmpFloats($a, '<=', $b) vs bccomp($a, $b) <= -1
  • 不需要依赖。
  • 适用于任何 PHP 版本。
  • 对负数有效。
  • 用你能想到的最长的小数。
  • 缺点: 比 bccomp ()稍慢

摘要

我来揭开谜底。

$a = 0.17;
$b = 1 - 0.83;// 0.17 (output)
// but actual value internally is: 0.17000000000000003996802888650563545525074005126953125
if($a == $b) {
echo 'same';
} else {
echo 'different';
}
// Output: different

因此,如果你尝试下面的方法,结果会是相等的:

if($b == 0.17000000000000003) {
echo 'same';
} else {
echo 'different';
}
// Output "same"

如何得到浮点的实际值?

$b = 1 - 0.83;
echo $b;// 0.17
echo number_format($a, 100);// 0.1700000000000000399680288865056354552507400512695312500000000000000000000000000000000000000000000000

你怎么能比较?

  1. 使用 BC 数学函数。(你仍然会得到很多 Wtf-aha-got cha 时刻)
  2. 您可以使用 PHP _ FLOAT _ EPSILON (PHP 7.2)尝试@Gladhon 的答案。
  3. 如果将 float 与 ==!=进行比较,您可以将它们类型化为字符串,那么它应该能够很好地工作:

使用 string 键入 cast:

$b = 1 - 0.83;
if((string)$b === (string)0.17) {
echo 'if';
} else {
echo 'else';
}
// it will output "if"

或者使用 number_format()进行类型转换:

$b = 1 - 0.83;
if(number_format($b, 3) === number_format(0.17, 3)) {
echo 'if';
} else {
echo 'else';
}
// it will output "if"

警告:

避免使用数学方法操作浮点数(乘法、除法等)然后进行比较的解决方案,大多数情况下,这些解决方案会解决一些问题,并引入其他问题。


建议解决方案

我已经创建了纯 PHP 函数(不需要依赖项/库/扩展)。检查和比较每个数字作为字符串。也适用于负数。

/**
* Compare numbers (floats, int, string), this function will compare them safely
* @param Float|Int|String  $a         (required) Left operand
* @param String            $operation (required) Operator, which can be: "==", "!=", ">", ">=", "<" or "<="
* @param Float|Int|String  $b         (required) Right operand
* @param Int               $decimals  (optional) Number of decimals to compare
* @return boolean                     Return true if operation against operands is matching, otherwise return false
* @throws Exception                   Throws exception error if passed invalid operator or decimal
*/
function cmpFloats($a, $operation, $b, $decimals = 15) {
if($decimals < 0) {
throw new Exception('Invalid $decimals ' . $decimals . '.');
}
if(!in_array($operation, ['==', '!=', '>', '>=', '<', '<='])) {
throw new Exception('Invalid $operation ' . $operation . '.');
}


$aInt = (int)$a;
$bInt = (int)$b;


$aIntLen = strlen((string)$aInt);
$bIntLen = strlen((string)$bInt);


// We'll not used number_format because it inaccurate with very long numbers, instead will use str_pad and manipulate it as string
$aStr = (string)$a;//number_format($a, $decimals, '.', '');
$bStr = (string)$b;//number_format($b, $decimals, '.', '');


// If passed null, empty or false, then it will be empty string. So change it to 0
if($aStr === '') {
$aStr = '0';
}
if($bStr === '') {
$bStr = '0';
}


if(strpos($aStr, '.') === false) {
$aStr .= '.';
}
if(strpos($bStr, '.') === false) {
$bStr .= '.';
}


$aIsNegative = strpos($aStr, '-') !== false;
$bIsNegative = strpos($bStr, '-') !== false;


// Append 0s to the right
$aStr = str_pad($aStr, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);
$bStr = str_pad($bStr, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);


// If $decimals are less than the existing float, truncate
$aStr = substr($aStr, 0, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals);
$bStr = substr($bStr, 0, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals);


$aDotPos = strpos($aStr, '.');
$bDotPos = strpos($bStr, '.');


// Get just the decimal without the int
$aDecStr = substr($aStr, $aDotPos + 1, $decimals);
$bDecStr = substr($bStr, $bDotPos + 1, $decimals);


$aDecLen = strlen($aDecStr);
//$bDecLen = strlen($bDecStr);


// To match 0.* against -0.*
$isBothZeroInts = $aInt == 0 && $bInt == 0;


if($operation === '==') {
return $aStr === $bStr ||
$isBothZeroInts && $aDecStr === $bDecStr;
} else if($operation === '!=') {
return $aStr !== $bStr ||
$isBothZeroInts && $aDecStr !== $bDecStr;
} else if($operation === '>') {
if($aInt > $bInt) {
return true;
} else if($aInt < $bInt) {
return false;
} else {// Ints equal, check decimals
if($aDecStr === $bDecStr) {
return false;
} else {
for($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];
if($aD > $bD) {
return true;
} else if($aD < $bD) {
return false;
}
}
}
}
} else if($operation === '>=') {
if($aInt > $bInt ||
$aStr === $bStr ||
$isBothZeroInts && $aDecStr === $bDecStr) {
return true;
} else if($aInt < $bInt) {
return false;
} else {// Ints equal, check decimals
if($aDecStr === $bDecStr) {// Decimals also equal
return true;
} else {
for($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];
if($aD > $bD) {
return true;
} else if($aD < $bD) {
return false;
}
}
}
}
} else if($operation === '<') {
if($aInt < $bInt) {
return true;
} else if($aInt > $bInt) {
return false;
} else {// Ints equal, check decimals
if($aDecStr === $bDecStr) {
return false;
} else {
for($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];
if($aD < $bD) {
return true;
} else if($aD > $bD) {
return false;
}
}
}
}
} else if($operation === '<=') {
if($aInt < $bInt ||
$aStr === $bStr ||
$isBothZeroInts && $aDecStr === $bDecStr) {
return true;
} else if($aInt > $bInt) {
return false;
} else {// Ints equal, check decimals
if($aDecStr === $bDecStr) {// Decimals also equal
return true;
} else {
for($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];
if($aD < $bD) {
return true;
} else if($aD > $bD) {
return false;
}
}
}
}
}
}

$a = 1 - 0.83;// 0.17
$b = 0.17;
if($a == $b) {
echo 'same';
} else {
echo 'different';
}
// Output: different (wrong)


if(cmpFloats($a, '==', $b)) {
echo 'same';
} else {
echo 'different';
}
// Output: same (correct)

下面是我个人库中一个处理浮点数的有用类。您可以根据自己的喜好对其进行微调,并在类方法中插入任何您喜欢的解决方案: ——)。

/**
* A class for dealing with PHP floating point values.
*
* @author Anthony E. Rutledge
* @version 12-06-2018
*/
final class Float extends Number
{
// PHP 7.4 allows for property type hints!


private const LESS_THAN = -1;
private const EQUAL = 0;
private const GREATER_THAN = 1;


public function __construct()
{


}


/**
* Determines if a value is an float.
*
* @param mixed $value
* @return bool
*/
public function isFloat($value): bool
{
return is_float($value);
}


/**
* A method that tests to see if two float values are equal.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function equals(float $y1, float $y2): bool
{
return (string) $y1 === (string) $y2;
}


/**
* A method that tests to see if two float values are not equal.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function isNotEqual(float $y1, float $y2): bool
{
return !$this->equals($y1, $y2);
}


/**
* Gets the bccomp result.
*
* @param float $y1
* @param float $y2
* @return int
*/
private function getBccompResult(float $y1, float $y2): int
{
$leftOperand = (string) $y1;
$rightOperand = (string) $y2;


// You should check the format of the float before using it.


return bccomp($leftOperand, $rightOperand);
}


/**
* A method that tests to see if y1 is less than y2.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function isLess(float $y1, float $y2): bool
{
return ($this->getBccompResult($y1, $y2) === self::LESS_THAN);
}


/**
* A method that tests to see if y1 is less than or equal to y2.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function isLessOrEqual(float $y1, float $y2): bool
{
$bccompResult = $this->getBccompResult($y1, $y2);
return ($bccompResult === self::LESS_THAN || $bccompResult === self::EQUALS);
}


/**
* A method that tests to see if y1 is greater than y2.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function isGreater(float $y1, float $y2): bool
{
return ($this->getBccompResult($y1, $y2) === self::GREATER_THAN);
}


/**
* A method that tests to see if y1 is greater than or equal to y2.
*
* @param float $y1
* @param float $y2
* @return bool
*/
public function isGreaterOrEqual(float $y1, float $y2): bool
{
$bccompResult = $this->getBccompResult($y1, $y2);
return ($bccompResult === self::GREATER_THAN || $bccompResult === self::EQUALS);
}


/**
* Returns a valid PHP float value, casting if necessary.
*
* @param mixed $value
* @return float
*
* @throws InvalidArgumentException
* @throws UnexpectedValueException
*/
public function getFloat($value): float
{
if (! (is_string($value) || is_int($value) || is_bool($value))) {
throw new InvalidArgumentException("$value should not be converted to float!");
}


if ($this->isFloat($value)) {
return $value;
}


$newValue = (float) $value;


if ($this->isNan($newValue)) {
throw new UnexpectedValueException("The value $value was converted to NaN!");
}


if (!$this->isNumber($newValue)) {
throw new UnexpectedValueException("The value $value was converted to something non-numeric!");
}


if (!$this->isFLoat($newValue)) {
throw new UnexpectedValueException("The value $value was not converted to a floating point value!");
}


return $newValue;
}
}
?>

来自 @ evilReiko的函数有一些像下面这样的错误:

cmpFloats(-0.1, '==', 0.1); // Expected: false, actual: true
cmpFloats(-0.1, '<', 0.1); // Expected: true, actual: false
cmpFloats(-4, '<', -3); // Expected: true, actual: true
cmpFloats(-5.004, '<', -5.003); // Expected: true, actual: false

在我的函数中,我已经修复了这些 bug,但无论如何,在某些情况下,这个函数返回了错误的答案:

cmpFloats(0.0000001, '==', -0.0000001); // Expected: false, actual: true
cmpFloats(843994202.303411, '<', 843994202.303413); // Expected: true, actual: false
cmpFloats(843994202.303413, '>', 843994202.303411); // Expected: true, actual: false

比较浮点数的固定函数

function cmpFloats($a, $operation, $b, $decimals = 15)
{
if ($decimals < 0) {
throw new Exception('Invalid $decimals ' . $decimals . '.');
}
if (!in_array($operation, ['==', '!=', '>', '>=', '<', '<='])) {
throw new Exception('Invalid $operation ' . $operation . '.');
}


$aInt = (int)$a;
$bInt = (int)$b;


$aIntLen = strlen((string)$aInt);
$bIntLen = strlen((string)$bInt);


// We'll not used number_format because it inaccurate with very long numbers, instead will use str_pad and manipulate it as string
$aStr = (string)$a;//number_format($a, $decimals, '.', '');
$bStr = (string)$b;//number_format($b, $decimals, '.', '');


// If passed null, empty or false, then it will be empty string. So change it to 0
if ($aStr === '') {
$aStr = '0';
}
if ($bStr === '') {
$bStr = '0';
}


if (strpos($aStr, '.') === false) {
$aStr .= '.';
}
if (strpos($bStr, '.') === false) {
$bStr .= '.';
}


$aIsNegative = strpos($aStr, '-') !== false;
$bIsNegative = strpos($bStr, '-') !== false;


// Append 0s to the right
$aStr = str_pad($aStr, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);
$bStr = str_pad($bStr, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals, '0', STR_PAD_RIGHT);


// If $decimals are less than the existing float, truncate
$aStr = substr($aStr, 0, ($aIsNegative ? 1 : 0) + $aIntLen + 1 + $decimals);
$bStr = substr($bStr, 0, ($bIsNegative ? 1 : 0) + $bIntLen + 1 + $decimals);


$aDotPos = strpos($aStr, '.');
$bDotPos = strpos($bStr, '.');


// Get just the decimal without the int
$aDecStr = substr($aStr, $aDotPos + 1, $decimals);
$bDecStr = substr($bStr, $bDotPos + 1, $decimals);


$aDecLen = strlen($aDecStr);
//$bDecLen = strlen($bDecStr);


// To match 0.* against -0.*
$isBothZeroInts = $aInt == 0 && $bInt == 0;


if ($operation === '==') {
return $aStr === $bStr ||
($isBothZeroInts && $aDecStr === $bDecStr && $aIsNegative === $bIsNegative);
} elseif ($operation === '!=') {
return $aStr !== $bStr ||
$isBothZeroInts && $aDecStr !== $bDecStr;
} elseif ($operation === '>') {
if ($aInt > $bInt) {
return true;
} elseif ($aInt < $bInt) {
return false;
} else {// Ints equal, check decimals
if ($aIsNegative !== $bIsNegative) {
return (!$aIsNegative && $bIsNegative);
}


if ($aDecStr === $bDecStr) {
return false;
} else {
for ($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];


if ($aIsNegative && $bIsNegative) {
if ($aD < $bD) {
return true;
} elseif ($aD > $bD) {
return false;
}
} else {
if ($aD > $bD) {
return true;
} elseif ($aD < $bD) {
return false;
}
}
}
}
}
} elseif ($operation === '>=') {
if ($aInt > $bInt ||
$aStr === $bStr ||
$isBothZeroInts && $aDecStr === $bDecStr) {
return true;
} elseif ($aInt < $bInt) {
return false;
} else {// Ints equal, check decimals
if ($aIsNegative !== $bIsNegative) {
return (!$aIsNegative && $bIsNegative);
}


if ($aDecStr === $bDecStr) {// Decimals also equal
return true;
} else {
for ($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];


if ($aIsNegative && $bIsNegative) {
if ($aD < $bD) {
return true;
} elseif ($aD > $bD) {
return false;
}
} else {
if ($aD > $bD) {
return true;
} elseif ($aD < $bD) {
return false;
}
}
}
}
}
} elseif ($operation === '<') {
if ($aInt < $bInt) {
return true;
} elseif ($aInt > $bInt) {
return false;
} else {// Ints equal, check decimals
if ($aIsNegative !== $bIsNegative) {
return ($aIsNegative && !$bIsNegative);
}


if ($aDecStr === $bDecStr) {
return false;
} else {
for ($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];


if ($aIsNegative && $bIsNegative) {
if ($aD > $bD) {
return true;
} elseif ($aD < $bD) {
return false;
}
} else {
if ($aD < $bD) {
return true;
} elseif ($aD > $bD) {
return false;
}
}
}
}
}
} elseif ($operation === '<=') {
if ($aInt < $bInt ||
$aStr === $bStr ||
$isBothZeroInts && $aDecStr === $bDecStr) {
return true;
} elseif ($aInt > $bInt) {
return false;
} else {// Ints equal, check decimals
if ($aIsNegative !== $bIsNegative) {
return ($aIsNegative && !$bIsNegative);
}


if ($aDecStr === $bDecStr) {// Decimals also equal
return true;
} else {
for ($i = 0; $i < $aDecLen; ++$i) {
$aD = (int)$aDecStr[$i];
$bD = (int)$bDecStr[$i];


if ($aIsNegative && $bIsNegative) {
if ($aD > $bD) {
return true;
} elseif ($aD < $bD) {
return false;
}
} else {
if ($aD < $bD) {
return true;
} elseif ($aD > $bD) {
return false;
}
}
}
}
}
}
}

回答你的问题

$a = 1 - 0.83;// 0.17
$b = 0.17;
if($a == $b) {
echo 'same';
} else {
echo 'different';
}
// Output: different (wrong)


if(cmpFloats($a, '==', $b)) {
echo 'same';
} else {
echo 'different';
}
// Output: same (correct)

答案很简单:

if( floatval( (string) $a ) >= floatval( (string) $b) ) { //do something }

使用. number _ format 可以很好地使用和决定小数位数

$a = 0.17;
$b = 1 - 0.83;  //0.17
$dec = 2;


if (number_format($a, $dec) == number_format($b, $dec)) {
echo 'a and b are same';
} else {
echo 'a and b are not same';
}

当两个变量的类型混合时,这种方法也可以工作(与字符串强制转换等其他方法相反) ,例如:

$a = '1.0000'; // Fetched from a DECIMAL db column
$b = 1;