web-dev-qa-db-fra.com

Java double comparaison epsilon

J'ai écrit une classe qui teste l'égalité, inférieure et supérieure à deux doubles en Java. Mon cas général compare le prix qui peut 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?

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

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

Ensuite, vous pouvez spécifier comment faire exactement l'arrondi (ce qui est parfois dicté par la loi dans les applications financières!) Et ne pas avoir à faire de hacks stupides comme cette chose epsilon.

Sérieusement, l'utilisation de types à virgule flottante pour représenter l'argent est extrêmement peu professionnelle.

101
Michael Borgwardt

Oui. Java double tiendra sa précision mieux que votre epsilon donné de 0,00001.

Toute erreur d'arrondi qui se produit en raison du stockage des valeurs à 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 dans Java = 1 x 10 ^ -5). 1E-6 est facile à distinguer de 1E-5 lors de la lecture de code alors que 0.00001 et 0.000001 semble si similaire quand on regarde le code, je pense qu'ils ont la même valeur.

11
Alex B

Whoa whoa whoa. Y a-t-il une raison spécifique pour laquelle vous utilisez la virgule flottante pour la devise, ou les choses seraient-elles mieux avec un précision arbitraire, format numérique à virgule fixe ? Je n'ai aucune idée du problème spécifique que vous essayez de résoudre, mais vous devez vous demander si un demi-centime est vraiment quelque chose avec lequel vous voulez travailler, ou s'il s'agit simplement d'un artefact lié à l'utilisation d'un format numérique imprécis.

8
Josh Lee

Si vous pouvez utiliser BigDecimal, alors 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);
}
6
carlosvin

Si vous avez affaire à de l'argent, je vous suggère de vérifier le modèle de conception Money (à l'origine de livre de Martin Fowler sur la conception architecturale d'entreprise ).

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

5
Yuval Adam

Bien que je sois d'accord avec l'idée que le double est mauvais pour l'argent, l'idée de comparer les doubles a tout de même un intérêt. En particulier, l'utilisation suggérée d'Epsilon ne convient qu'aux nombres dans une plage spécifique. Voici une utilisation plus générale d'un epsilon, par rapport au rapport des deux nombres (le test de 0 est omis):

boolean equal(double d1, double d2) {
  double d = d1 / d2;
  return (Math.abs(d - 1.0) < 0.001);
}
2
Bill

Les nombres à virgule flottante ont seulement autant de chiffres significatifs, mais ils peuvent aller beaucoup plus haut. Si votre application gère 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 000 si vous utilisez la virgule flottante et le double. Ce n'est pas une bonne représentation de l'argent, sauf si vous êtes sacrément sûr de ne jamais gérer plus d'un million de dollars dans ce système.

1
Karl

Cents? Si vous calculez des valeurs monétaires, vous ne devriez vraiment pas utiliser de valeurs flottantes. L'argent est en fait des valeurs dénombrables. Les centimes ou pennys, etc. pourraient être considérés comme les deux (ou peu importe) chiffres les moins significatifs d'un entier. Vous pouvez stocker et calculer les valeurs monétaires sous forme d'entiers et les diviser par 100 (par exemple, placez un point ou une virgule deux avant les deux derniers chiffres). L'utilisation de flotteurs peut entraîner d'étranges erreurs d'arrondi ...

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

1
Stein G. Strindhaug

Comme d'autres commentateurs l'ont correctement noté, vous ne devez jamais utiliser l'arithmétique à virgule flottante lorsque des valeurs exactes sont requises, comme pour les valeurs monétaires. La raison principale est en effet le comportement d'arrondi inhérent aux virgules flottantes, mais n'oublions pas que traiter des virgules flottantes signifie également avoir à traiter avec 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 regardez si le résultat est égal à 10.0 - ce qu'il ne devrait pas être, car la différence n'est clairement pas inférieure à 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 de bits significatifs lors de la soustraction si deux valeurs à virgule flottante ont des exposants différents.

Si vous pensez appliquer une approche plus avancée de la "différence relative" comme suggéré par d'autres commentateurs, vous devriez lire l'excellent article de Bruce Dawson Comparing Floating Point Numbers, 2012 Edition , qui montre que cette approche a des défauts similaires et qu'il n'y a en fait aucune comparaison de virgule flottante à sécurité intégrée qui fonctionne pour toutes les plages de nombres à virgule flottante.

Pour faire court: abstenez-vous de doubles pour les valeurs monétaires et utilisez des représentations numériques exactes 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ébordements et les débordements. Cela donne une valeur maximale représentable de 9'223'372'036'854'775.807, ce qui devrait être suffisant pour la plupart des applications du monde réel.

0
Franz D.