188

I want to compare two floats in PHP, like in this sample code:

$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';
}

In this code it returns the result of the else condition instead of the if condition, even though $a and $b are same. Is there any special way to handle/compare floats in PHP?

If yes then please help me to solve this issue.

Or is there a problem with my server config?

mskfisher
  • 3,291
  • 4
  • 35
  • 48
Santosh S
  • 4,165
  • 6
  • 32
  • 37
  • I get `a and b are same`. Is this your full code? – Pekka Jun 30 '10 at 11:55
  • what version? it works fine for me. – gblazex Jun 30 '10 at 11:55
  • @Andrey this is probably it because the real world case is likely to be more complex than the example quoted. Why not add it as an answer? – Pekka Jun 30 '10 at 11:59
  • 2
    Did you read the `floating-point` tag description? http://stackoverflow.com/tags/floating-point/info That's a behavior you'd likely encounter in any programming language, when using floating-point numbers. See e.g. http://stackoverflow.com/questions/588004/is-javascripts-math-broken – Piskvor left the building Mar 29 '12 at 20:37

17 Answers17

263

If you do it like this they should be the same. But note that a characteristic of floating-point values is that calculations which seem to result in the same value do not need to actually be identical. So if $a is a literal .17 and $b arrives there through a calculation it can well be that they are different, albeit both display the same value.

Usually you never compare floating-point values for equality like this, you need to use a smallest acceptable difference:

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

Something like that.

Joey
  • 344,408
  • 85
  • 689
  • 683
  • 24
    BEWARE! Choosing a fixed epsilon is a bad way just because it looks small, this comparison will return true in alot of precision errors when the numbers are small. A correct way would be to check if the relative error is smaller than the epsilon. `abs($a-$b)` > `abs(($a-$b)/$b)` – Zaqwsx Jun 21 '13 at 13:18
  • Yes, I intentionally simplified here. – Joey Jun 21 '13 at 19:34
  • So let me understand, PHP prints both numbers the same way but internally considers them different, right? This is quite weird, because I sopose if internally they are different, PHP should also print them different, so we can see the difference and not waste time. Know what I mean? Does it make any sense? – Alexandru Trandafir Catalin Mar 20 '14 at 09:48
  • 1
    @Alexandru: I know what you mean, but PHP isn't alone in that regard. You need to distinguish two use cases here: Showing a number to a user. In that case displaying `0.10000000000000000555111512312578270211815834045410156` is usually pointless and they'd prefer `0.1` instead. And writing a number so that it can be read again in exactly the same way. As you see, it's not as clear-cut as you make it out to be. And for the record, you still want to compare floating-point numbers like I have shown because you can arrive at `$a` and `$b` via different calculations that can make them different. – Joey Mar 20 '14 at 10:13
  • 2
    There are still some edge cases which this test fails. Such as `a=b=0` and if `a` is the smallest possible none zero positive value and `b` is the smallest possible non zero negative value, the test will incorrectly fail. Some good information here: http://floating-point-gui.de/errors/comparison/ – Dom Jun 14 '16 at 11:48
  • 19
    Why divide on `$b` ? the [PHP manual](http://php.net/manual/en/language.types.float.php) just did `if(abs($a-$b) < $epsilon) ` the [MySQL manual](https://dev.mysql.com/doc/refman/5.7/en/problems-with-float.html) also did the same `HAVING ABS(a - b) <= 0.0001` – Accountant م May 01 '17 at 18:51
  • I agree with Accountant, this is not correct answer because you are dividing with $b. – Caslav Sabani Aug 31 '18 at 10:01
  • 2
    @CaslavSabani: This is relative, not absolute error. It's still broken (especially when `$a == $b == 0`, but it's already a lot more general than absolute error. If `$a` and `$b` are in the millions, then your `EPSILON` would have to be very different than if `$a` and `$b` are somewhere close to `0`. See Dom's link above for a better discussion of this. – Joey Aug 31 '18 at 10:16
  • that thing with dividing by b to get relative error actually that piete suggested doesn't work – user151496 Oct 09 '19 at 14:01
100

Read the red warning in the manual first. You must never compare floats for equality. You should use the epsilon technique.

For example:

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

where PHP_FLOAT_EPSILON is constant representing a very small number (you have to define it in old versions of PHP before 7.2)

miken32
  • 42,008
  • 16
  • 111
  • 154
Andrey
  • 59,039
  • 12
  • 119
  • 163
  • 3
    To clarify, is EPSILON in this case the machine epsilon, which is roughly 2.2204460492503E-16? And, does this comparison work for two floats of any magnitude? – Michael Cordingley Apr 17 '15 at 20:20
  • 1
    @MichaelCordingley No, `EPSILON` here is an arbitrary user-defined constant. PHP does not have a built-in constant representing an architecture's specific idea of epsilon. (See also [`get_defined_constants`](http://php.net/manual/en/function.get-defined-constants.php).) – bishop Nov 23 '15 at 19:23
  • 5
    [`PHP_FLOAT_EPSILON`](http://php.net/manual/en/reserved.constants.php) Smallest representable positive number x, so that x + 1.0 != 1.0. Available as of PHP 7.2.0. – Code4R7 Sep 28 '17 at 20:24
  • 7
    This does not actually work in this case: $a = 270.10 + 20.10; $b = 290.20; if (abs($a-$b) < PHP_FLOAT_EPSILON){ echo 'same'; } – NemoXP Nov 07 '19 at 12:07
  • @NemoXP because those expressions produce different numbers. `echo $a - $b; /* 5.6843418860808E-14 */ echo PHP_FLOAT_EPSILON; /* 2.2204460492503E-16 */` The question is how you want to define "equal" for your application, how close the numbers should be to be considered equal. – Andrey Nov 12 '19 at 09:46
  • the PHP EPSILON is too small to work, it's seems simple calculations generate a bigger delta than that epsilon, returning false for something that is pretty close but not as close as the minimum allowed in PHP. – Pablo Pazos Aug 13 '20 at 16:50
35

Or try to use bc math functions:

<?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 );

Result:

0.17 == 0.17 (core comp oper): bool(false)
0.17 == 0.17 (with bc func)  : bool(true)
miken32
  • 42,008
  • 16
  • 111
  • 154
Mario
  • 502
  • 4
  • 4
  • 2
    in your usage of bccomp you've missed the "scale" thus you're actually comparing 0 to 0 according to the manual: http://php.net/manual/en/function.bccomp.php – stefancarlton Jan 23 '16 at 01:21
  • I'm liking this. Most solutions seem to rely on rounding and losing precision, but I'm dealing with latitude and longitude coordinates with 12 points of precision and this seems to compare them accurately with no tweaking needed. – Rikaelus Jun 12 '16 at 07:23
  • What about performance? [`bccomp()`](http://php.net/manual/en/function.bccomp.php) takes strings as arguments. Anyway you could use [`PHP_FLOAT_DIG`](http://php.net/manual/en/reserved.constants.php) for the scale argument. – Code4R7 Sep 28 '17 at 20:29
23

As said before, be very careful when doing floating point comparisons (whether equal-to, greater-than, or less-than) in PHP. However if you're only ever interested in a few significant digits, you can do something like:

$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';
}

The use of rounding to 2 decimal places (or 3, or 4) will cause the expected result.

Pang
  • 9,564
  • 146
  • 81
  • 122
Michael Butler
  • 6,079
  • 3
  • 38
  • 46
  • 3
    Extra word of warning, I wouldn't recommend littering your codebase with statements like these. If you want to do a loose float comparison, make a method like `loose_float_compare` so it's obvious what is going on. – Michael Butler May 14 '18 at 14:11
  • PHP's native `bccomp($a, $b, 2)` is superior to your solution. In this example, the 2 is the precision. you can set it to whatever number of floating points you want to compare. – John Miller Feb 07 '20 at 07:07
  • 1
    @JohnMiller I'm not disagreeing with you, but bccomp isn't available by default. It requires either a compilation flag to be enabled, or an extension installed. Not part of core. – Michael Butler Apr 28 '20 at 18:07
22

It would be better to use native PHP comparison:

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. 

Returns 0 if the two operands are equal, 1 if the left_operand is larger than the right_operand, -1 otherwise.

Pang
  • 9,564
  • 146
  • 81
  • 122
FieryCat
  • 1,875
  • 18
  • 28
  • 3
    It's worth noting that the bc functions all truncate, rather than round. You may be expecting, for example, `bccomp(0.9999999, 1.000000, 2)` to return zero, but this will return -1. – Cynewulf Apr 08 '21 at 12:57
  • 1
    @Cynewulf, for me it's a correct behavior. If not in yours, use PHP_FLOAT_EPSILON – FieryCat Apr 08 '21 at 16:19
21

If you have floating point values to compare to equality, a simple way to avoid the risk of internal rounding strategy of the OS, language, processor or so on, is to compare the string representation of the values.

You can use any of the following to produce the desired result: https://3v4l.org/rUrEq

String Type Casting

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

String Concatenation

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

strval function

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

String representations are much less finicky than floats when it comes to checking equality.

Will B.
  • 17,883
  • 4
  • 67
  • 69
Ame Nomade
  • 327
  • 2
  • 3
  • or if ( strval($a) === strval($b)) { … } if you don't want to convert the original values – Yehor Mar 18 '19 at 14:23
  • Well, my original answer was: if (''.$a === ''.$b) { … } but someone edited it. So... – Ame Nomade Mar 19 '19 at 16:34
  • 1
    @Ekonoval Could you please elaborate to your modification, It appears that you are asserting that the `(string)` cast operation is performed by-reference, changing the original declaration? If so that is not the case https://3v4l.org/Craas – Will B. Apr 19 '20 at 00:18
  • @fyrye Yeah I guess I was wrong, the both approaches yield the same result. – Yehor Apr 20 '20 at 09:06
  • Updated the answer to give an example usage and all of the examples of the other edits along with the original – Will B. Apr 20 '20 at 14:24
  • 1
    Note that this will not always work as expected when mixing types! Try with `$a = '1.000'; $b = 1;` which is often the case when a DECIMAL is fetched from db, while the other value is calculated – the_nuts May 24 '22 at 08:52
7

This works for me on 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
}
Pang
  • 9,564
  • 146
  • 81
  • 122
crmpicco
  • 16,605
  • 26
  • 134
  • 210
5

If you have a small, finite number of decimal points that will be acceptable, the following works nicely (albeit with slower performance than the epsilon solution):

$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';
}
Alive to die - Anant
  • 70,531
  • 10
  • 51
  • 98
dtbarne
  • 8,110
  • 5
  • 43
  • 49
4

Here is the solution for comparing floating points or decimal numbers

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

Cast a decimal variable to string and you will be fine.

Alive to die - Anant
  • 70,531
  • 10
  • 51
  • 98
Natalie Rey
  • 51
  • 1
  • 3
4

For PHP 7.2, you can work with PHP_FLOAT_EPSILON ( http://php.net/manual/en/reserved.constants.php ):

if(abs($a-$b) < PHP_FLOAT_EPSILON){
   echo 'a and b are same';
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Gladhon
  • 211
  • 2
  • 5
  • Good solution. But: 1- Requires updating PHP 7.2 which not everyone can do easily for existing big/old systems 2- This works only for `==` and `!=` but not `>`, `>=`, `<`, `<=` – evilReiko Mar 13 '19 at 07:36
3

Works great using and deciding the number of decimal places to use. 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';
}

This will also work (contrary to other methods such as string cast) in cases when the type of the two variables is mixed, such as:

$a = '1.0000'; // Fetched from a DECIMAL db column
$b = 1;
the_nuts
  • 5,634
  • 1
  • 36
  • 68
PM1625637
  • 102
  • 10
2

Comparing floats for equality has a naive O(n) algorithm.

You must convert each float value to a string, then compare each digit starting from the left side of each float's string representation using integer comparison operators. PHP will autocast the digit in each index position to an integer before the comparison. The first digit larger than the other will break the loop and declare the float that it belongs to as the greater of the two. On average, there will be 1/2 * n comparisons. For floats equal to each other, there will be n comparisons. This is the worst case scenario for the algorithm. The best case scenario is that the first digit of each float is different, causing only one comparison.

You cannot use INTEGER COMPARISON OPERATORS on raw float values with the intention of generating useful results. The results of such operations have no meaning because you are not comparing integers. You are violating the domain of each operator which generates meaningless results. This holds for delta comparison as well.

Use integer comparison operators for what they are designed for : comparing integers.

SIMPLIFIED SOLUTION:

<?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;
  }
 }

?>
Kyle
  • 21
  • 1
2

2019

TL;DR

Use my function below, like this if(cmpFloats($a, '==', $b)) { ... }

  • Easy to read/write/change: cmpFloats($a, '<=', $b) vs bccomp($a, $b) <= -1
  • No dependencies needed.
  • Works with any PHP version.
  • Works with negative numbers.
  • Works with the longest decimal you can imagine.
  • Downside: Slightly slower than bccomp()

Summary

I'll unveil the mystery.

$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

So if you try the below, it will be equal:

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

How to get the actual value of float?

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

How can you compare?

  1. Use BC Math functions. (you'll still get a lot of wtf-aha-gotcha moments)
  2. You may try @Gladhon's answer, using PHP_FLOAT_EPSILON (PHP 7.2).
  3. If comparing floats with == and !=, you can typecast them to strings, it should work perfectly:

Type cast with string:

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

Or typecast with 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"

Warning:

Avoid solutions that involves manipulating floats mathematically (multiplying, dividing, etc) then comparing, mostly they'll solve some problems and introduce other problems.


Suggested Solution

I've create pure PHP function (no depenedcies/libraries/extensions needed). Checks and compares each digit as string. Also works with negative numbers.

/**
 * 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)
evilReiko
  • 19,501
  • 24
  • 86
  • 102
2

If you write it just like that it will probably work, so I imagine you've simplified it for the question. (And keeping the question simple and concise is normally a very good thing.)

But in this case I imagine one result is a calculation and one result is a constant.

This violates a cardinal rule of floating point programming: Never do equality comparisons.

The reasons for this are a bit subtle1 but what's important to remember is that they usually don't work (except, ironically, for integral values) and that the alternative is a fuzzy comparison along the lines of:

if abs(a - y) < epsilon



1. One of the major problems involves the way we write numbers in programs. We write them as decimal strings, and as a result most of the fractions we write do not have exact machine representations. They don't have exact finite forms because they repeat in binary. Every machine fraction is a rational number of the form x/2n. Now, the constants are decimal and every decimal constant is a rational number of the form x/(2n * 5m). The 5m numbers are odd, so there isn't a 2n factor for any of them. Only when m == 0 is there a finite representation in both the binary and decimal expansion of the fraction. So, 1.25 is exact because it's 5/(22*50) but 0.1 is not because it's 1/(20*51). In fact, in the series 1.01 .. 1.99 only 3 of the numbers are exactly representable: 1.25, 1.50, and 1.75.

DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
  • DigitalRoss its quite difficult to understand few terms in your comment, but yes, its very informative. And I am going to google these terms. Thanks :) – Santosh S Mar 26 '12 at 06:52
  • Isn't it safe enough to do comparisons on floats, provided you are rounding the result every time and are within a few significant digits? In other words `round($float, 3) == round($other, 3)` – Michael Butler Jul 21 '14 at 18:00
1

Function from @evilReiko have some bugs like these:

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

In my function I've fixed these bugs, but anyway in some cases this function return wrong answers:

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

Fixed function for compare floats

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;
                        }
                    }
                }
            }
        }
    }
}

Answer for your question

$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)
Volodymyr
  • 490
  • 5
  • 9
1

Simple answer:

if( floatval( (string) $a ) >= floatval( (string) $b) ) { //do something }
Nader
  • 336
  • 4
  • 12
  • 1
    If PHP can do their job properly on evaluating float value, there is no way people make long process to compare float value here. – poring91 Jun 17 '21 at 23:56
0

Here is a useful class from my personal library for dealing with floating point numbers. You can tweek it to your liking and insert any solution you like into the class methods :-).

/**
 * 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;
    }
}
?>
Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44