Compare floats in php

后端 未结 16 1669
清歌不尽
清歌不尽 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)
    

提交回复
热议问题