IT story

PHP에서 float 비교

hot-time 2020. 6. 22. 07:39
반응형

PHP에서 float 비교


이 샘플 코드와 같이 PHP에서 두 개의 float를 비교하고 싶습니다.

$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에서 float를 처리 / 비교하는 특별한 방법이 있습니까?

그렇다면이 문제를 해결하도록 도와주세요.

또는 서버 구성에 문제가 있습니까?


당신이 이것을 이렇게하면 그들은 동일 해야 합니다. 그러나 참고 부동 소수점 값의 특성은 계산 것을 보인다 실제로 동일 할 필요는 없습니다 같은 값이 발생할 수 있습니다. 따라서 $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 버전에서는 정의해야 함)


또는 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)

앞에서 언급했듯이 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 비교 를 사용하는 것이 좋습니다 .

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을, 그렇지 않으면 -1을 리턴합니다.


평등과 비교할 부동 소수점 값이있는 경우 OS, 언어, 프로세서 등 내부 반올림 전략의 위험을 피하는 간단한 방법 은 다음 과 같이 값 문자열 표현 을 비교하는 것입니다.

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

이것은 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
}

부동 소수점 또는 소수를 비교하는 솔루션은 다음과 같습니다.

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

decimal변수를 캐스트하면 string괜찮을 것입니다.


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

당신이 그것을 좋아하는 것처럼 작성한다면 아마도 효과가있을 것이기 때문에 질문을 위해 그것을 단순화했다고 상상합니다. (질문을 간단하고 간결하게 유지하는 것은 일반적으로 매우 좋은 일입니다.)

그러나이 경우 하나의 결과가 계산이고 하나의 결과가 상수라고 생각합니다.

이는 부동 소수점 프로그래밍의 기본 규칙을 위반합니다 . 평등 비교를 수행하지 마십시오.

이것에 대한 이유는 약간 미묘 하지만 1 기억해야 할 것은 일반적으로 작동하지 않으며 (아이러니하게, 정수 값을 제외하고) 대안은 다음 줄에 대한 퍼지 비교입니다.

if abs(a - y) < epsilon



1. 주요 문제 중 하나는 프로그램에서 숫자를 쓰는 방식과 관련이 있습니다. 우리는 그것들을 십진 문자열로 쓰고, 결과적으로 우리가 쓰는 대부분의 분수는 정확한 기계 표현을 가지고 있지 않습니다. 이진수로 반복되므로 정확한 유한 형태를 갖지 않습니다. 모든 기계 분수는 x / 2 n 형식의 합리적인 수입니다 . 이제 상수는 10 진수이며 모든 10 진수 상수는 x / (2 n * 5 m ) 형식의 유리수 입니다. 5 숫자이므로 2가 아닌, 홀수 n 개의 그들 중 어떤 요소. m == 0 일 때만 분수의 이진 확장과 십진 확장 모두에 유한 표현이 있습니다. 따라서 1.25는 5 / (2 2 * 5 0 이므로 정확합니다.)이지만 0.1은 1 / (2 0 * 5 1 ) 이기 때문에 아닙니다 . 실제로 1.01 .. 1.99 시리즈에서는 숫자 중 3 개만 정확하게 표현할 수 있습니다 : 1.25, 1.50 및 1.75.


동등성을 위해 float를 비교하는 것은 순진한 O (n) 알고리즘이 있습니다.

각 float 값을 문자열로 변환 한 다음 정수 비교 연산자를 사용하여 각 float 문자열 표현의 왼쪽부터 시작하여 각 숫자를 비교해야합니다. PHP는 비교하기 전에 각 인덱스 위치의 숫자를 정수로 자동 전송합니다. 첫 번째 숫자가 다른 숫자보다 크면 루프가 끊어지고 실수가 두 개 중 큰 것으로 선언됩니다. 평균적으로 1/2 * n 비교가 있습니다. 서로 같은 수레의 경우 n 개의 비교가 있습니다. 이것은 알고리즘에 대한 최악의 시나리오입니다. 가장 좋은 시나리오는 각 부동 소수점의 첫 번째 숫자가 다르므로 한 번만 비교하는 것입니다.

유용한 결과를 생성하려는 의도로 원시 부동 소수점 값에는 정수 비교 연산자를 사용할 수 없습니다. 정수를 비교하지 않기 때문에 이러한 연산의 결과는 의미가 없습니다. 의미없는 결과를 생성하는 각 연산자의 도메인을 위반하고 있습니다. 이것은 델타 비교에도 적용됩니다.

정수 비교 연산자를 위해 설계된 것에 대해 정수 비교 연산자를 사용하십시오.

단순화 된 솔루션 :

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

?>

2019 년

TL; DR

아래처럼 내 기능을 사용하십시오. if(cmpFloats($a, '==', $b)) { ... }

  • 읽기 / 쓰기 / 변경이 쉬움 : cmpFloats($a, '<=', $b)vsbccomp($a, $b) <= -1
  • 의존성이 필요하지 않습니다.
  • 모든 PHP 버전에서 작동합니다.
  • 음수와 함께 작동합니다.
  • 상상할 수있는 가장 긴 10 진수로 작동합니다.
  • 단점 : 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"

float의 실제 값을 얻는 방법?

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

어떻게 비교할 수 있습니까?

  1. BC Math 함수를 사용하십시오 . (여전히 wtf-aha-gotcha 순간이 많이 나옵니다)
  2. PHP_FLOAT_EPSILON (PHP 7.2)을 사용하여 @Gladhon의 답변을 시도 할 수 있습니다.
  3. float와 ==and를 비교 !=하면 문자열로 타입 캐스트 할 수 있으면 완벽하게 작동합니다.

문자열로 타입 캐스트 :

$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 기능을 만들었습니다 (depenedcies / library / extensions 필요 없음). 각 숫자를 확인하고 문자열로 비교합니다. 음수로도 작동합니다.

/**
 * 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)

참고 URL : https://stackoverflow.com/questions/3148937/compare-floats-in-php

반응형