2008-12-10 8 views
34

J'ai écrit une classe qui teste l'égalité, inférieure à, et supérieure à deux doubles en Java. Mon cas général est la comparaison des prix qui peuvent avoir une précision d'un demi-cent. 59.005 contre 59.395. L'epsilon que j'ai choisi est-il adéquat pour ces cas?Java double comparaison 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

Vous avez réveillé la colère de certaines personnes ici! Voir ici si vous voulez vraiment utiliser des nombres à virgule flottante: http://docs.sun.com/source/806-3568/ncg_goldberg.html – Loki

+2

D'autres problèmes mis à part, réduisent les risques d'erreur de codage en supprimant le code dupliqué. La première méthode statique devient return égal à (a, b, EPSILON); – nslntmnx

+3

Parlant juste de la beauté, 'a == b? true: x' peut être remplacé par la version beaucoup plus agréable et plus facile à lire 'a == b || x'. – Matthias

Répondre

10

Oui. Les doubles Java tiendront leur précision mieux que votre epsilon donné de 0.00001.

Toute erreur d'arrondi qui se produit en raison du stockage des valeurs en virgule flottante se produira plus petit que 0,00001. J'utilise régulièrement 1E-6 ou 0.000001 pour un double epsilon en Java sans problème. Sur une note connexe, j'aime le format de epsilon = 1E-5; parce que je pense qu'il est plus lisible (1E-5 en Java = 1 x 10^-5). 1E-6 est facile à distinguer de 1E-5 lors de la lecture du code alors que 0.00001 et 0.000001 ont l'air si similaires en regardant le code que je pense qu'ils ont la même valeur.

7

Whoa whoa whoa. Y a-t-il une raison spécifique pour laquelle vous utilisez le flottant pour la devise, ou est-ce que les choses seraient mieux avec un arbitrary-precision, fixed-point number format? Je n'ai aucune idée de ce que le problème que vous essayez de résoudre est, mais vous devriez penser si un demi-cent est vraiment quelque chose que vous voulez travailler, ou si c'est juste un artefact d'utiliser un format numérique imprécis.

90

Vous n'utilisez PAS le double pour représenter de l'argent. Jamais. Utilisez java.math.BigDecimal à la place.

Ensuite, vous pouvez spécifier exactement comment arrondir (ce qui est parfois dicté par la loi dans les applications financières!) Et ne pas avoir à faire des hacks stupides comme ce truc epsilon. Sérieusement, l'utilisation de types à virgule flottante pour représenter l'argent est extrêmement peu professionnelle.

+54

+1 car en effet vous n'utilisez jamais de nombres à virgule flottante pour représenter de l'argent mais -1 (donc je n'ai pas modifié votre compte) car utiliser un epsilon n'est pas un "hack stupide". C'est quelque chose de fondamental dans le calcul scientifique, pas un "hack stupide". Le document de Goldberg sur le sujet est d'accord sur celui-là. – SyntaxT3rr0r

+47

Sérieusement, vous ne devriez pas supposer que ce n'est que parce que c'est ainsi que vous faites les choses que c'est le meilleur moyen dans tous les cas. Ayant travaillé dans quatre banques différentes, je n'ai jamais vu un système de trading utilisant BigDecimal, et je ne recommanderais pas de les utiliser. –

+2

Peter, que recommanderiez-vous pour de l'argent à la place? Ma préférence serait longue. combinaison à base courte pour une classe Money. Cependant, je suis extrêmement hésitant à lancer le mien pour la situation. Je l'ai déjà fait ... mais ce n'est pas quelque chose que je peux prouver qui fonctionne. – monksy

1

Les nombres à virgule flottante ont seulement autant de chiffres significatifs, mais ils peuvent aller beaucoup plus haut. Si votre application traite un grand nombre, vous remarquerez que la valeur epsilon doit être différente.

0,001 + 0,001 = 0,002 MAIS 12.345.678.900.000.000.000.000 + 1 = 12.345.678.900.000.000.000.000 si vous utilisez à virgule flottante et double. Ce n'est pas une bonne représentation de l'argent, à moins que vous soyez sacrément sûr de ne jamais gérer plus d'un million de dollars dans ce système.

+0

Le point flottant ne représente pas exactement des valeurs telles que 0,1 car, en interne, il stocke la valeur sous la forme 2^exposant * (1 + fraction). Même dans une fourchette raisonnable, comme 0,001 + 0,001. Exécutez "print int (1.13 * 100.0)/100.0" si vous avez perl. Il renvoie 1.12. –

1

Cents? Si vous calculez des valeurs monétaires, vous ne devriez pas utiliser de valeurs flottantes. L'argent est en fait des valeurs dénombrables. Les centimes ou centimes, etc. peuvent être considérés comme les deux (ou n'importe quels) chiffres les moins significatifs d'un nombre entier. Vous pouvez stocker et calculer des valeurs monétaires sous la forme d'entiers et diviser par 100 (par exemple placer un point ou une virgule deux avant les deux derniers chiffres). L'utilisation de flotteur peut conduire à des erreurs d'arrondi étranges ...

Quoi qu'il en soit, si votre epsilon est censé définir la précision, il semble un peu trop petit (trop précis) ...

5

Si vous faites affaire avec de l'argent Je suggère de vérifier le modèle de conception de Money (à l'origine de Martin Fowler's book on enterprise architectural design).

Je vous suggère de lire ce lien pour la motivation: http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2

+2

Le serveur de moredesignpatterns semble avoir disparu et n'a pas été remplacé. L'article est sur archive.org, cependant: http://web.archive.org/web/20090105214308/http://wiki.moredesignpatterns.com/space/Value%2BObject%2BMotivation%2Bv2 –

2

Bien que je suis d'accord avec l'idée que le double est mauvais pour l'argent, toujours l'idée de comparer double a un intérêt. En particulier, l'utilisation suggérée d'epsilon est seulement adaptée aux nombres dans une gamme spécifique.Voici une utilisation plus générale d'un epsilon, par rapport au rapport des deux nombres (test pour 0 omis):

booléen égal (double d1, double d2) { double d = d1/d2; return (Math.abs (d-1,0),001); }

+1

C'est très dangereux en raison de zéro division. – lethalman

+0

En effet, '0.000001' et' 0' ne seraient pas égaux avec ce code. – Joey

4

Si vous pouvez utiliser BigDecimal, utilisez-le, sinon:

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

Cela ne devrait-il pas être Double.compare (Math.abs (a-b), Math.pow (10, -precision))? – Michael

0

Comme d'autres commentateurs correctement noté, vous devriez jamais utiliser l'arithmétique à virgule flottante lorsque les valeurs exactes sont requises, telles que pour valeurs monétaires. La raison principale est en effet le comportement d'arrondi inhérent aux points flottants, mais n'oublions pas que traiter avec des points flottants signifie aussi avoir à faire face à des valeurs infinies et NaN.

Pour illustrer que votre approche ne fonctionne tout simplement pas, voici un code de test simple. J'ajoute simplement votre EPSILON à 10.0 et regarder si le résultat est égal à 10.0 - qui ne devrait pas être, comme la différence est clairement pas moins que EPSILON:

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

Surprise:

ERROR: 10.0 == 10.00001 

Les erreurs se produisent en raison de la perte si des bits significatifs sur la soustraction si deux valeurs à virgule flottante ont des exposants différents.

Si vous pensez à appliquer une approche « différence relative » plus avancée comme suggéré par d'autres commentateurs, vous devriez lire l'excellent article de Bruce Dawson Comparing Floating Point Numbers, 2012 Edition, ce qui montre que cette approche a des lacunes similaires et qu'il n'y a en fait pas FAIL comparaison approximative sans danger à virgule flottante qui fonctionne pour toutes les plages de nombres à virgule flottante.

Pour faire court: s'abstenir de double s pour les valeurs monétaires, et utiliser des représentations exactes des nombres telles que BigDecimal. Par souci d'efficacité, vous pouvez également utiliser longs interprété comme "millis" (dixièmes de centimes), tant que vous évitez de manière fiable les dépassements et les sous-flux. Cela donne un maximum de valeurs représentables de 9'223'372'036'854'775.807, ce qui devrait être suffisant pour la plupart des applications du monde réel.