2008-12-10 9 views
34

Ich schrieb eine Klasse, die auf Gleichheit prüft, weniger als und größer als mit zwei Doppel in Java. Mein allgemeiner Fall ist der Vergleich von Preisen, die eine Genauigkeit von einem halben Cent haben können. 59.005 im Vergleich zu 59.395. Ist das Epsilon, das ich gewählt habe, für diese Fälle angemessen?Java doppelten Vergleich epsilon

private final static double EPSILON = 0.00001; 


/** 
* Returns true if two doubles are considered equal. Tests if the absolute 
* difference between two doubles has a difference less then .00001. This 
* should be fine when comparing prices, because prices have a precision of 
* .001. 
* 
* @param a double to compare. 
* @param b double to compare. 
* @return true true if two doubles are considered equal. 
*/ 
public static boolean equals(double a, double b){ 
    return a == b ? true : Math.abs(a - b) < EPSILON; 
} 


/** 
* Returns true if two doubles are considered equal. Tests if the absolute 
* difference between the two doubles has a difference less then a given 
* double (epsilon). Determining the given epsilon is highly dependant on the 
* precision of the doubles that are being compared. 
* 
* @param a double to compare. 
* @param b double to compare 
* @param epsilon double which is compared to the absolute difference of two 
* doubles to determine if they are equal. 
* @return true if a is considered equal to b. 
*/ 
public static boolean equals(double a, double b, double epsilon){ 
    return a == b ? true : Math.abs(a - b) < epsilon; 
} 


/** 
* Returns true if the first double is considered greater than the second 
* double. Test if the difference of first minus second is greater then 
* .00001. This should be fine when comparing prices, because prices have a 
* precision of .001. 
* 
* @param a first double 
* @param b second double 
* @return true if the first double is considered greater than the second 
*    double 
*/ 
public static boolean greaterThan(double a, double b){ 
    return greaterThan(a, b, EPSILON); 
} 


/** 
* Returns true if the first double is considered greater than the second 
* double. Test if the difference of first minus second is greater then 
* a given double (epsilon). Determining the given epsilon is highly 
* dependant on the precision of the doubles that are being compared. 
* 
* @param a first double 
* @param b second double 
* @return true if the first double is considered greater than the second 
*    double 
*/ 
public static boolean greaterThan(double a, double b, double epsilon){ 
    return a - b > epsilon; 
} 


/** 
* Returns true if the first double is considered less than the second 
* double. Test if the difference of second minus first is greater then 
* .00001. This should be fine when comparing prices, because prices have a 
* precision of .001. 
* 
* @param a first double 
* @param b second double 
* @return true if the first double is considered less than the second 
*    double 
*/ 
public static boolean lessThan(double a, double b){ 
    return lessThan(a, b, EPSILON); 
} 


/** 
* Returns true if the first double is considered less than the second 
* double. Test if the difference of second minus first is greater then 
* a given double (epsilon). Determining the given epsilon is highly 
* dependant on the precision of the doubles that are being compared. 
* 
* @param a first double 
* @param b second double 
* @return true if the first double is considered less than the second 
*    double 
*/ 
public static boolean lessThan(double a, double b, double epsilon){ 
    return b - a > epsilon; 
} 
+3

Sie haben den Zorn einiger Leute hier erweckt! Sehen Sie hier, wenn Sie wirklich Gleitkommazahlen verwenden möchten: http://docs.sun.com/source/806-3568/ncg_goldberg.html – Loki

+2

Andere Probleme beiseite, reduzieren die Wahrscheinlichkeit von Codierungsfehlern durch Entfernen von doppeltem Code. Erste statische Methode wird Return gleich (a, b, EPSILON); – nslntmnx

+3

Apropos schön, 'a == b? true: x 'kann durch die viel nettere und leichter zu lesende Version 'a == b || ersetzt werden x'. – Matthias

Antwort

10

Ja. Java-Doubles halten ihre Präzision besser als Ihr gegebenes Epsilon von 0,00001.

Jeder Rundungsfehler, der aufgrund der Speicherung von Fließkommawerten auftritt, wird kleiner als 0,00001 sein. Ich verwende regelmäßig 1E-6 oder 0.000001 für ein Doppel-Epsilon in Java ohne Probleme.

Zu einer verwandten Notiz, ich mag das Format epsilon = 1E-5;, weil ich fühle, dass es lesbarer ist (1E-5 in Java = 1 x 10^-5). 1E-6 ist beim Lesen von Code leicht von 1E-5 zu unterscheiden, während 0,00001 und 0,000001 bei einem Blick auf den Code so ähnlich aussehen, dass sie denselben Wert haben.

7

Whoa whoa whoa. Gibt es einen bestimmten Grund, warum Sie Gleitkomma für Währung verwenden, oder wären die Dinge mit einem arbitrary-precision, fixed-point number format besser? Ich habe keine Ahnung, was das spezifische Problem ist, das Sie zu lösen versuchen, aber Sie sollten darüber nachdenken, ob ein halber Cent wirklich etwas ist, mit dem Sie arbeiten möchten oder ob es nur ein Artefakt ist, ein ungenaues Zahlenformat zu verwenden.

90

Sie verwenden nicht doppelt, um Geld darzustellen. Niemals. Verwenden Sie stattdessen java.math.BigDecimal.

Dann können Sie angeben, wie genau Rundung (die manchmal gesetzlich in Finanzanwendungen vorgeschrieben ist!) Und müssen nicht dumme Hacks wie diese Epsilon-Sache zu tun.

Im Ernst, mit Gleitkommatypen, um Geld darzustellen ist extrem unprofessionell.

+54

+1, weil Sie in der Tat nicht immer Gleitkommazahlen verwenden, um Geld darzustellen, sondern -1 (also habe ich Ihre Zählung nicht geändert), weil die Verwendung eines Epsilons kaum ein "dummer Hack" ist. Es ist etwas Grundlegendes im wissenschaftlichen Rechnen, kein "blöder Hack". Goldbergs Papier zu diesem Thema stimmt dem zu. – SyntaxT3rr0r

+47

Ernsthaft, Sie sollten nicht davon ausgehen, dass nur weil Sie so Dinge tun, dass es in allen Fällen der beste Weg ist. Nachdem ich bei vier verschiedenen Banken gearbeitet habe, habe ich noch nie ein Handelssystem gesehen, das BigDecimal verwendet, noch würde ich empfehlen, sie zu verwenden. –

+2

Peter, was würden Sie stattdessen für Geld empfehlen? Meine Vorliebe wäre ein Long. Kurzbasierte Kombination für eine Money-Klasse. Ich bin jedoch sehr zögerlich, meine eigene für die Situation zu rollen. Ich habe es schon vorher gemacht ... aber es ist nicht etwas, was ich beweisen kann. – monksy

1

Fließkommazahlen haben nur so viele signifikante Stellen, aber sie können viel höher gehen. Wenn Ihre App jemals mit großen Zahlen umgehen wird, werden Sie feststellen, dass der Epsilon-Wert unterschiedlich sein sollte.

0,001 + 0,001 = 0,002 BUT 12.345.678.900.000.000.000.000 + 1 = 12.345.678.900.000.000.000.000 wenn Sie Gleitkomma- und Doppel verwenden. Es ist keine gute Darstellung von Geld, es sei denn, Sie sind verdammt sicher, dass Sie nie mehr als eine Million Dollar in diesem System handhaben werden.

+0

Der Gleitkommawert repräsentiert Werte wie 0,1 nicht genau, da intern der Wert als 2^Exponent * (1 + Bruch) gespeichert wird. Sogar in einem vernünftigen Bereich wie 0,001 + 0,001. Führen Sie "print int (1.13 * 100.0)/100.0" aus, wenn Sie perl haben. Es gibt 1.12 zurück. –

1

Cent? Wenn Sie Geldwerte berechnen, sollten Sie keine Float-Werte verwenden. Geld ist tatsächlich zählbare Werte. Die Cents oder Pennys usw. könnten als die zwei (oder was auch immer) niedrigstwertigen Stellen einer ganzen Zahl betrachtet werden. Sie können Geldwerte als ganze Zahlen speichern und berechnen und durch 100 dividieren (z. B. Punkt oder Komma zwei vor den beiden letzten Ziffern). führen zu seltsamen Rundungsfehlern unter Verwendung von Float kann ...

Wie auch immer, wenn Ihr epsilon soll die Genauigkeit definieren, es sieht ein bisschen zu klein (zu genau) ...

5

Wenn Sie mit Geld zu tun Ich schlage vor, das Money-Entwurfsmuster zu überprüfen (ursprünglich von Martin Fowler's book on enterprise architectural design).

ich diesen Link für die Motivation empfehlen Lesen: http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2

+2

Der Server moresdesignpatterns scheint verschwunden zu sein und wurde nicht ersetzt. Der Artikel ist jedoch auf archive.org: http://web.archive.org/web/20090105214308/http://wiki.moredesignpatterns.com/space/Value%2BObject%2BMotivation%2Bv2 –

2

Während ich mit der Idee einverstanden, die für Geld ist schlecht verdoppeln, immer noch die Idee des Vergleichens verdoppelt hat Interesse. Insbesondere ist die vorgeschlagene Verwendung von Epsilon nur für Zahlen in einem bestimmten Bereich geeignet.Hier ist eine allgemeine Verwendung eines Epsilons, bezogen auf das Verhältnis der beiden Zahlen (Test für 0 weggelassen):

Boolesche gleich (Doppel-d1, Doppel-d2) { Doppel-d = d1/d2; Rückkehr (Math.abs (d - 1.0) < 0.001); }

+1

Das ist sehr gefährlich wegen Null Aufteilung. – lethalman

+0

In der Tat wären '0.000001' und' 0' nicht identisch mit diesem Code. – Joey

4

Wenn Sie BigDecimal verwenden können, dann verwenden, sonst:

/** 
    *@param precision number of decimal digits 
    */ 
public static boolean areEqualDouble(double a, double b, int precision) { 
    return Math.abs(a - b) <= Math.pow(10, -precision); 
} 
+0

Sollte das nicht Double.compare (Math.abs (a-b), Math.pow (10, -präzision)) sein? – Michael

0

Wie andere commenters richtig erwähnt, sollten Sie nie Verwendung Gleitkommaarithmetik wenn genaue Werte erforderlich sind, wie zum Beispiel für Geldwerte. Der Hauptgrund ist in der Tat das Rundungsverhalten, das Fließkomma-Punkten innewohnt, aber vergessen wir nicht, dass der Umgang mit Gleitkomma-Punkten auch mit unendlichen und NaN-Werten zu tun hat.

Zur Veranschaulichung, dass Ihr Ansatz einfach nicht funktioniert, hier ist ein einfacher Testcode. Ich fügen Sie einfach Ihre EPSILON zu 10.0 und schauen, ob das Ergebnis zu 10.0 gleich ist - was es nicht sein sollte, da die Differenz ist eindeutig nicht weniger alsEPSILON:

double a = 10.0; 
    double b = 10.0 + EPSILON; 
    if (!equals(a, b)) { 
     System.out.println("OK: " + a + " != " + b); 
    } else { 
     System.out.println("ERROR: " + a + " == " + b); 
    } 

Überraschung:

ERROR: 10.0 == 10.00001 

Der Fehler tritt aufgrund des Verlusts auf, wenn signifikante Bits bei Subtraktion, wenn zwei Fließkommawerte unterschiedliche Exponenten haben.

Wenn Sie denken, eine erweiterte „relative Differenz“ Ansatzes der Anwendung wie von anderen commen vorgeschlagen, sollen Sie Bruce Dawson ausgezeichneten Artikel Comparing Floating Point Numbers, 2012 Edition gelesen, die zeigt, dass dieser Ansatz Ähnliche Mängel hat, und dass es tatsächlich kein fehler sicherer ungefährer Gleitkommavergleich, der für alle Bereiche von Fließkommazahlen funktioniert.

Um die Dinge kurz zu machen: Entfernen Sie von double s für monetäre Werte, und verwenden Sie genaue Zahlen Darstellungen wie BigDecimal. Aus Gründen der Effizienz könnten Sie auch longs interpretiert als "Millis" (Zehntel Cent) verwenden, solange Sie Über- und Unterläufe sicher verhindern. Dies ergibt maximal darstellbare Werte von 9'223'372'036'854'775.807, was für die meisten realen Anwendungen ausreichen sollte.