Porównaj pływaki w php

156

Chcę porównać dwie zmiennoprzecinkowe w PHP, jak w tym przykładowym kodzie:

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

W tym kodzie zwraca wynik elsewarunku zamiast ifwarunku, mimo że $ai $bsą takie same. Czy jest jakiś specjalny sposób obsługi / porównywania wartości zmiennoprzecinkowych w PHP?

Jeśli tak, pomóż mi rozwiązać ten problem.

Czy jest problem z konfiguracją mojego serwera?

Santosh Sonarikar
źródło
Rozumiem a and b are same. Czy to twój pełny kod?
Pekka
jaka wersja? to działa dobrze dla mnie.
gblazex
@Andrey, prawdopodobnie tak jest, ponieważ realny przypadek może być bardziej złożony niż przytoczony przykład. Dlaczego nie dodać tego jako odpowiedzi?
Pekka
2
Czy przeczytałeś floating-pointopis tagu? stackoverflow.com/tags/floating-point/info To zachowanie, które prawdopodobnie napotkasz w dowolnym języku programowania, używając liczb zmiennoprzecinkowych. Patrz np stackoverflow.com/questions/588004/is-javascripts-math-broken
Piskvor opuścił budynek

Odpowiedzi:

232

Jeśli robisz to w ten sposób, powinny być takie same. Należy jednak zauważyć, że cechą charakterystyczną wartości zmiennoprzecinkowych jest to, że obliczenia, które wydają się dawać tę samą wartość, nie muszą być w rzeczywistości identyczne. Więc jeśli $ajest literałem .17i $bdociera do niego poprzez obliczenia, może być tak, że są różne, chociaż oba mają tę samą wartość.

Zwykle nigdy nie porównujesz wartości zmiennoprzecinkowych dla równości w ten sposób, musisz użyć najmniejszej akceptowalnej różnicy:

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

Coś w tym stylu.

Joey
źródło
21
STRZEC SIĘ! Wybór stałego epsilon to zły sposób tylko dlatego, że wygląda na mały, to porównanie zwróci prawdę z wieloma błędami precyzji, gdy liczby są małe. Prawidłowym sposobem byłoby sprawdzenie, czy błąd względny jest mniejszy niż epsilon. abs($a-$b)>abs(($a-$b)/$b)
Piet Bijl
1
@Alexandru: Wiem, co masz na myśli, ale PHP nie jest sam w tym względzie. Musisz tutaj rozróżnić dwa przypadki użycia: Pokazywanie numeru użytkownikowi. W takim przypadku wyświetlanie 0.10000000000000000555111512312578270211815834045410156jest zwykle bezcelowe i woleliby 0.1zamiast tego. I zapisanie liczby, aby można ją było odczytać ponownie w dokładnie ten sam sposób. Jak widzisz, nie jest to tak wyraźne, jak się wydaje. A dla przypomnienia, nadal chcesz porównać liczb zmiennoprzecinkowych jak wykazałem, ponieważ można dotrzeć $ai $bza pośrednictwem różnych obliczeń, które czynią je różni.
Joey
2
Wciąż istnieją skrajne przypadki, w których ten test kończy się niepowodzeniem. Takich jak a=b=0i jeśli ajest najmniejszą możliwą wartością dodatnią zero i bjest najmniejszą możliwą wartością ujemną niezerową, test niepoprawnie zakończy się niepowodzeniem. Kilka dobrych informacji tutaj: floating-point-gui.de/errors/comparison
Dom
13
Po co dzielić $b? obsługi PHP po prostu nie obsługi MySQL zrobił również taka samaif(abs($a-$b) < $epsilon) HAVING ABS(a - b) <= 0.0001
Księgowa م
1
@CaslavSabani: To jest błąd względny, a nie bezwzględny. Nadal jest zepsuty (zwłaszcza gdy $a == $b == 0, ale jest już o wiele bardziej ogólny niż błąd bezwzględny. Jeśli $ai $bsą w milionach, to EPSILONmusielibyście być zupełnie inni niż gdyby $ai $bbyli gdzieś blisko) 0. Zobacz link Dom powyżej, aby lepiej omówić to.
Joey
64

Najpierw przeczytaj czerwone ostrzeżenie w instrukcji . Nigdy nie możesz porównywać zmiennych dla równości. Powinieneś użyć techniki epsilon.

Na przykład:

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

gdzie PHP_FLOAT_EPSILONjest stała reprezentująca bardzo małą liczbę (musisz ją zdefiniować w starszych wersjach PHP przed 7.2)

Andrey
źródło
2
Aby wyjaśnić, czy EPSILON w tym przypadku jest maszyną epsilon, która jest w przybliżeniu 2,2204460492503E-16? I czy to porównanie działa dla dwóch wartości zmiennoprzecinkowych dowolnej wielkości?
Michael Cordingley,
1
@MichaelCordingley Nie, EPSILONtutaj jest dowolna stała zdefiniowana przez użytkownika. PHP nie ma wbudowanej stałej reprezentującej konkretną koncepcję architektury epsilon. (Zobacz także get_defined_constants)
biskup
5
PHP_FLOAT_EPSILONNajmniejsza reprezentowalna liczba dodatnia x, tak że x + 1,0! = 1,0. Dostępne od PHP 7.2.0.
Code4R7
2
To faktycznie nie działa w tym przypadku: $ a = 270,10 + 20,10; $ b = 290,20; if (abs ($ a- $ b) <PHP_FLOAT_EPSILON) {echo 'same'; }
NemoXP
@NemoXP, ponieważ te wyrażenia dają różne liczby. echo $a - $b; /* 5.6843418860808E-14 */ echo PHP_FLOAT_EPSILON; /* 2.2204460492503E-16 */Pytanie brzmi, jak chcesz zdefiniować „równe” dla swojej aplikacji, jak blisko liczby powinny być uważane za równe.
Andrey
28

Lub spróbuj użyć funkcji matematycznych 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 );

Wynik:

0.17 == 0.17 (core comp oper): bool(false)
0.17 == 0.17 (with bc func)  : bool(true)
Mario
źródło
2
podczas korzystania z bccomp przegapiłeś "skalę", więc tak naprawdę porównujesz 0 do 0 zgodnie z instrukcją: php.net/manual/en/function.bccomp.php
stefancarlton
Podoba mi się to. Większość rozwiązań wydaje się polegać na zaokrąglaniu i utracie precyzji, ale mam do czynienia ze współrzędnymi szerokości i długości geograficznej z 12 punktami dokładności, a to wydaje się dokładnie je porównywać bez konieczności poprawiania.
Rikaelus
A co z wydajnością? bccomp()przyjmuje ciągi jako argumenty. W każdym razie możesz użyć PHP_FLOAT_DIGargumentu skali.
Code4R7
19

Jak wspomniano wcześniej, należy być bardzo ostrożnym podczas wykonywania porównań zmiennoprzecinkowych (równych, większych lub mniejszych niż) w PHP. Jeśli jednak interesuje Cię tylko kilka znaczących cyfr, możesz zrobić coś takiego:

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

Użycie zaokrąglenia do 2 miejsc po przecinku (lub 3 lub 4) da oczekiwany wynik.

Michael Butler
źródło
1
Dodatkowe ostrzeżenie, nie polecałbym zaśmiecania bazy kodu takimi stwierdzeniami. Jeśli chcesz zrobić luźne porównanie zmiennoprzecinkowe, utwórz taką metodę, loose_float_compareaby było oczywiste, co się dzieje.
Michael Butler
Język PHP bccomp($a, $b, 2)jest lepszy od Twojego rozwiązania. W tym przykładzie 2 to precyzja. możesz ustawić dowolną liczbę punktów zmiennoprzecinkowych, które chcesz porównać.
John Miller
@JohnMiller Nie zgadzam się z tobą, ale bccomp nie jest domyślnie dostępny. Wymaga włączenia flagi kompilacji lub zainstalowania rozszerzenia. Nie jest częścią rdzenia.
Michael Butler
17

Lepiej byłoby użyć natywnego porównania 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. 

Zwraca 0, jeśli dwa operandy są równe, 1, jeśli lewy_operand jest większy niż prawy_operand, -1 w przeciwnym razie.

FieryCat
źródło
10

Jeśli masz wartości zmiennoprzecinkowe do porównania z równością, prostym sposobem uniknięcia ryzyka wewnętrznej strategii zaokrąglania systemu operacyjnego, języka, procesora itp. Jest porównanie ciągowej reprezentacji wartości.

Aby uzyskać pożądany wynik, możesz użyć dowolnego z poniższych: https://3v4l.org/rUrEq

Rzutowanie typu ciągów

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

Konkatenacja ciągów

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

funkcja strval

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

Reprezentacje ciągów są znacznie mniej skomplikowane niż zmiennoprzecinkowe, jeśli chodzi o sprawdzanie równości.

Ame Nomade
źródło
lub if (strval ($ a) === strval ($ b)) {…} jeśli nie chcesz konwertować oryginalnych wartości
Ekonoval
Cóż, moja pierwotna odpowiedź brzmiała: if (''. $ A === ''. $ B) {…}, ale ktoś to edytował. Więc ...
Ame Nomade,
1
@Ekonoval Czy mógłbyś rozwinąć swoją modyfikację? Wygląda na to, że twierdzisz, że (string)operacja rzutowania jest wykonywana przez odniesienie, zmieniając pierwotną deklarację? Jeśli tak, to nie jest to przypadek 3v4l.org/Craas
fyrye
@fyrye Tak, chyba się myliłem, oba podejścia dają ten sam wynik.
Ekonoval
Zaktualizowano odpowiedź, aby podać przykładowe użycie i wszystkie przykłady innych edycji wraz z oryginałem
fyrye
4

Jeśli masz małą, skończoną liczbę miejsc po przecinku, które będą akceptowalne, poniższe działa dobrze (choć z wolniejszą wydajnością niż rozwiązanie epsilon):

$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';
}
dtbarne
źródło
4

To działa dla mnie na 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
}
crmpicco
źródło
3

W PHP 7.2 możesz pracować z PHP_FLOAT_EPSILON ( http://php.net/manual/en/reserved.constants.php ):

if(abs($a-$b) < PHP_FLOAT_EPSILON){
   echo 'a and b are same';
}
Gladhon
źródło
Dobre rozwiązanie. Ale: 1 Wymaga aktualizacji PHP 7.2, które nie każdy może zrobić łatwo za istniejące duże / starych systemów 2- to działa tylko dla ==a !=jednak nie >, >=, <,<=
evilReiko
2

Jeśli napiszesz to tak po prostu, prawdopodobnie zadziała, więc wyobrażam sobie, że uprościłeś to pytanie. (A zachowanie prostego i zwięzłego pytania jest zazwyczaj bardzo dobrą rzeczą).

Ale w tym przypadku wyobrażam sobie, że jeden wynik jest obliczeniem, a jeden wynik jest stałą.

Narusza to podstawową zasadę programowania zmiennoprzecinkowego: nigdy nie porównuj równości.

Powody tego są nieco subtelne 1, ale ważne jest, aby pamiętać, że zwykle nie działają (z wyjątkiem, jak na ironię, wartości całkowitych) i że alternatywą jest niejasne porównanie wzdłuż linii:

if abs(a - y) < epsilon



1. Jeden z głównych problemów dotyczy sposobu, w jaki zapisujemy liczby w programach. Zapisujemy je jako ciągi dziesiętne, w wyniku czego większość zapisywanych ułamków nie ma dokładnej reprezentacji maszyny. Nie mają dokładnych form skończonych, ponieważ powtarzają się binarnie. Każdy ułamek maszynowy jest liczbą wymierną w postaci x / 2 n . Teraz stałe są dziesiętne, a każda stała dziesiętna jest liczbą wymierną w postaci x / (2 n * 5 m ). Liczby 5 m są nieparzyste, więc dla żadnej z nich nie ma współczynnika 2 n . Tylko wtedy, gdy m == 0 istnieje skończona reprezentacja zarówno w binarnym, jak i dziesiętnym rozszerzeniu ułamka. Czyli 1,25 jest dokładne, ponieważ wynosi 5 / (2 2 * 5 0), ale 0,1 nie jest, ponieważ wynosi 1 / (2 0 * 5 1 ). W rzeczywistości w serii 1.01 ... 1,99 tylko 3 z liczb są dokładnie reprezentowalne: 1,25, 1,50 i 1,75.

DigitalRoss
źródło
DigitalRoss jest dość trudny do zrozumienia kilku terminów w twoim komentarzu, ale tak, jest bardzo pouczający. I mam zamiar wygooglować te warunki. Dzięki :)
Santosh Sonarikar
Czy nie jest wystarczająco bezpieczne wykonywanie porównań na liczbach zmiennoprzecinkowych, pod warunkiem, że za każdym razem zaokrągla się wynik i mieści się w granicach kilku znaczących cyfr? Innymi słowyround($float, 3) == round($other, 3)
Michael Butler
2

Oto rozwiązanie do porównywania liczb zmiennoprzecinkowych lub dziesiętnych

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

Prześlij decimalzmienną na stringi wszystko będzie dobrze.

Natalie Rey
źródło
1

Porównywanie liczb zmiennoprzecinkowych dla równości ma naiwny algorytm O (n).

Musisz przekonwertować każdą wartość zmiennoprzecinkową na łańcuch, a następnie porównać każdą cyfrę, zaczynając od lewej strony reprezentacji ciągu każdego zmiennoprzecinkowego, używając operatorów porównania liczb całkowitych. Przed porównaniem PHP automatycznie dokasuje cyfrę w każdej pozycji indeksu do liczby całkowitej. Pierwsza cyfra większa od drugiej przerwie pętlę i zadeklaruje liczbę zmiennoprzecinkową, do której należy, jako większą z nich. Średnio będzie 1/2 * n porównań. Dla równych sobie liczb zmiennoprzecinkowych będzie n porównań. To jest najgorszy scenariusz dla algorytmu. Najlepszym scenariuszem jest to, że pierwsza cyfra każdej liczby zmiennoprzecinkowej jest inna, co powoduje tylko jedno porównanie.

Nie można używać INTEGER COMPARISON OPERATORS na surowych wartościach zmiennoprzecinkowych w celu wygenerowania użytecznych wyników. Wyniki takich operacji nie mają znaczenia, ponieważ nie porównujesz liczb całkowitych. Naruszasz domenę każdego operatora, która generuje bezsensowne wyniki. Dotyczy to również porównania delta.

Używaj operatorów porównania liczb całkowitych do tego, do czego są przeznaczone: do porównywania liczb całkowitych.

UPROSZCZONE ROZWIĄZANIE:

<?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
źródło
1

2019

TL; DR

Użyj mojej funkcji poniżej, w ten sposób if(cmpFloats($a, '==', $b)) { ... }

  • Łatwy do odczytu / zapisu / zmiany: cmpFloats($a, '<=', $b)vsbccomp($a, $b) <= -1
  • Żadne zależności nie są potrzebne.
  • Działa z każdą wersją PHP.
  • Działa z liczbami ujemnymi.
  • Działa z najdłuższym miejscem dziesiętnym, jaki możesz sobie wyobrazić.
  • Wada: nieco wolniej niż bccomp ()

Podsumowanie

Odsłonię tajemnicę.

$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

Więc jeśli spróbujesz poniżej, będzie to równe:

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

Jak uzyskać rzeczywistą wartość float?

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

Jak możesz porównać?

  1. Użyj funkcji BC Math . (nadal będziesz mieć wiele chwil wtf-aha-gotcha)
  2. Możesz spróbować odpowiedzi @ Gladhon, używając PHP_FLOAT_EPSILON (PHP 7.2).
  3. Jeśli porównujesz zmiennoprzecinkowe z ==i !=, możesz je typecastować na łańcuchy, powinno to działać idealnie:

Typ cast ze sznurkiem :

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

Lub typoszereg z 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"

Ostrzeżenie:

Unikaj rozwiązań, które wymagają matematycznego manipulowania liczbami zmiennoprzecinkowymi (mnożenie, dzielenie itp.), A następnie porównywanie, w większości rozwiązują one pewne problemy i wprowadzają inne problemy.


Sugerowane rozwiązanie

Stworzyłem czystą funkcję PHP (nie są potrzebne żadne zależności / biblioteki / rozszerzenia). Sprawdza i porównuje każdą cyfrę jako ciąg. Działa również z liczbami ujemnymi.

/**
 * 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
źródło
1

Funkcja z @evilReiko ma kilka błędów, takich jak te:

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

W mojej funkcji naprawiłem te błędy, ale i tak w niektórych przypadkach ta funkcja zwraca błędne odpowiedzi:

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

Naprawiono funkcję porównywania pływaków

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

Odpowiedz na Twoje pytanie

$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
źródło
0

Oto przydatna klasa z mojej osobistej biblioteki do radzenia sobie z liczbami zmiennoprzecinkowymi. Możesz dostosować to do swoich upodobań i wstawić dowolne rozwiązanie do metod klasowych :-).

/**
 * 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
źródło
0

Prosta odpowiedź:

if( floatval( (string) $a ) >= floatval( (string) $b) ) { //do something }
Nader
źródło