PHP Float Werte vergleichen; auf Gleichheit prüfen | Rechenoperationen mit Float
Will man in PHP Rechenoperationen mit Floats durchführen, gilt es einiges zu beachten. Dieser Artikel zeigt damit verbundene Probleme auf und stellt Lösungsansätze und Workarounds vor.
Aufgrund der endlichen Mantisse in der Fließkommadarstellung lassen sich solche Zahlen auf einem Computer nicht beliebig genau darstellen. Es handelt sich bei Fließkommazahlen (engl.: "Floats") um Näherungswerte, deren Genauigkeit abhängig ist von der voreingestellten "Precision" - die Anzahl der Nachkommastellen, die zur Betrachtung eines Float Wertes herangezogen werden. Standardwert für PHP ist 14.
Übersicht
- 1. Vergleich-von-Float-Werten
- 1.1. Maschinengenauigkeit
- 1.2. Vergleich von 2 Floats als Strings
- 2. Precision
- 3. Cast
- 3.1. cast float -> zu -> integer
- 4. float in einer for - Schleife
- 5. Rechenoperationen mit Float Werten im Allgemeinen
- 5.1. Workaround
1. Vergleich von Float-Werten
Ein if ($fA !== $fB){..}
funktioniert nicht, falls $fA
und $fB
Float Werte sind.
Daher niemals Float Werte miteinander vergleichen. So auch die offizielle Empfehlung.
Orginal Zitat PHP:
"[…] So never trust floating number results to the last digit, and do not compare floating point numbers directly for equality. If higher precision is necessary, the arbitrary precision math functions and gmp functions are available. […]"
Quelle: php.net "language.types.float"
1.1. Maschinengenauigkeit "machine epsilon"
Der offizelle Workaround bzgl. eines Vergleichs von Float Werten mittels Berücksichtigung des sog. machine epsilon
funktioniert nicht gut genug, wie Tests zeigten. Hier wird im Prinzip eine Abweichung der beiden Werte um einen Toleranz-Wert (der machine epsilon
) geprüft; s.: php.net "language.types.float Comparison"
$fA = 1.23456789;
$fB = 1.23456780;
$fEpsilon = 0.00001;
if (abs($fA - $fB) < $fEpsilon) {
echo "true";
}
Bei der Standard-Precision von 14 gibt es ab Float-Wert 84 starke Abweichungen (84 wird dann bspw. zu 83.999995 usw; getestet 2014-02-12). Prüfe daher auf "Gleichheit" mittels Stringvergleich:
1.2. Vergleich von 2 Floats als Strings
Ein "Vergleich" von Floats kann erfolgen, indem man die Zahlen als Strings betrachtet. Hierzu kann man die PHP Funktion strcmp (string compare) heranziehen:
strcmp ("$fA", "$fB");
Die Funktion liefert einen Integer Wert zurück:
0: beide Zahlen (Strings) sind gleich
>0: $fA ist größer > als $fB
<0: $fA ist kleiner < als $fB
2. Precision
Es muss immer eine Precision von 14
gelten. Gemeint ist die Genauigkeit von 14 Nachkommastellen, wobei 14 auch Standard / Default Einstellung ist (Stand 2014-02-12, PHP 5.3.10).
Wird eine höhere Precision angegeben, erhöht sich schnell die Anzahl der Nachkommastellen bei Floatwerten.
Aus float(40)
wird dann bspw. float(39.99999999995)
So kann die Precision zur Laufzeit gesetzt werden:
ini_set('precision', '14');
3. Cast
3.1. cast float -> zu -> integer
Einen Float Wert niemals direkt nach integer casten.
Das funktioniert nicht:
// gibt 60342 - also falsch (das Ergebnis der Multiplikation ist ebenfalls ein float)
(int) (603.43 * 100);
Stattdessen den Umweg über eine Stringbetrachtung gehen
- Zahl erst als String ausdrücken: "$fValue", und diesen Ausdruck sicher nach float casten: ((float) "$fValue")
- Dann diesen Float wiederum als String ausdrücken (durch anhängen eines Leerstrings ."")
- Den String dann mit "intval" zu integer casten
Beispiel:
$fValue = 603.43;
$iInteger = intval((((float) "$fValue") * 100 ) . '')
var_dump($iInteger); // liefert int(60343) - korrekt
Beispiel ohne eine Rechnung:
$fValue = 603.43;
$iInteger = intval (((float) "$fValue") . '');
var_dump($iInteger); // liefert int(603) - korrekt
4. float in einer for - Schleife
Funktioniert nicht beständig, da hier eine Rechenoperation mit 2 Float-Werten vorgenommen wird - nämlich $fA
und 0.1
-> also $fA+= 0.1
. Die Schleife liefert schnell unerwartete Werte.
for ($fA = 0; $fA < 60; $fA+= 0.1)
{
var_dump($fA);
}
Ausgabe:
[..]
float(53.8)
float(53.9)
float(54)
float(54.1)
float(54.200000000001)
float(54.300000000001)
float(54.400000000001)
float(54.500000000001)
float(54.600000000001)
float(54.700000000001)
[..]
Lösung: Die Rechenoperation so ausdrücken, dass der inkrementelle Float als String betrachtet wird - nämlich "$fA". Sodann liegt keine Rechenoperation zwischen 2 Float-Werten vor, sondern zwischen einem String und einem Float Wert (In diesem Fall nutzen wir die fehlende Typsicherheit / Stringenz von PHP aus):
for ($fA = 0; $fA <= 60; $fA = "$fA" + 0.1)
{
var_dump($fA);
}
5. Rechenoperationen mit Float Werten im Allgemeinen
Es zeigt sich, dass Rechenoperationen mit 2 oder mehreren Float Werten problematisch sind und nicht belastbare Ergebnisse liefern können. Ein weiteres Beispiel soll dies noch einmal erläutern:
$fA = 0.1;
$fB = 0.7;
$fSum = ($fA + $fB);
var_dump($fSum); // OK: liefert float(0.8)
$fMulti = ($fSum * 10); // OK: liefert float(8)
var_dump($fMulti);
var_dump( floor($fMulti) ); // VORSICHT ! liefert float(7)
Erklärung: Gegeben sind 2 Float-Werte $fA
und $fB
. Die erste Rechenoperation (Addition) ($fA + $fB)
liefert wie erwartet den Float 0.8
- Bei einer Standard-Precision von 14 wohlgemerkt. Wird jedoch bspw. eine Precision von 16 gesetzt (also eine höhere Genauigkeit, da nun 16 Nachkommastellen berücksichtigt werden), so ändert sich das Ergebnis auf: float(0.7999999999999999)
. Siehe dazu auch weiter oben den Abschnitt "Precision".
Die zweite Rechenoperation (Multiplikation) liefert ebenfalls wie erwartet float(8)
.
Die Ausgabe mit floor()
nun jedoch liefert nicht wie erwartet den Wert float(8)
, sondern float(7)
! Dies ist so, da die interne Repräsentation des Float-Wertes (womit letztendlich gerechnet wird) eine andere ist.
5.1. Workaround
Als Workaround kann man Float Werte als Strings ausdrücken und dann diese zur Berechnung heranziehen. Wie schon weiter oben vorweggenommen, nutzen wir hier die fehlende Typsicherheit / Stringenz von PHP aus.
Das komplette obere Beispiel mit als String ausgedrückten Floats:
$fA = 0.1;
$fB = 0.7;
$fSum = ("$fA" + "$fB");
var_dump($fSum); // OK: liefert float(0.8)
$fMulti = ("$fSum" * 10); // OK: liefert float(8)
var_dump($fMulti);
var_dump( floor("$fMulti") ); // OK: liefert float(8)
Hier werden nun
- erwartete Ergebnisse geliefert
- auch der Typ ist der richtige.
Links zum Thema