Compare floats in php

后端 未结 16 1657
清歌不尽
清歌不尽 2020-11-22 00:47

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         


        
相关标签:
16条回答
  • 2020-11-22 01:23

    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)
    
    0 讨论(0)
  • 2020-11-22 01:29

    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.

    0 讨论(0)
  • 2020-11-22 01:29

    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.

    0 讨论(0)
  • 2020-11-22 01:30

    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.

    0 讨论(0)
  • 2020-11-22 01:31

    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
    }
    
    0 讨论(0)
  • 2020-11-22 01:32

    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.

    0 讨论(0)
提交回复
热议问题