2008-12-10 24 views
34

Escribí una clase que prueba la igualdad, menor que, y mayor que con dos dobles en Java. Mi caso general es comparar precios que pueden tener una precisión de medio centavo. 59.005 comparado con 59.395. ¿El épsilon que elegí es adecuado para esos casos?Java comparación doble 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

¡Ha despertado la ira de algunas personas aquí! Consulte aquí si realmente desea usar números de coma flotante: http://docs.sun.com/source/806-3568/ncg_goldberg.html – Loki

+2

Aparte de otras cuestiones, reduzca las posibilidades de errores de codificación eliminando el código duplicado. El primer método estático se vuelve igual a (a, b, EPSILON); – nslntmnx

+3

Hablando de belleza, 'a == b? true: x' se puede reemplazar por la versión mucho más agradable y fácil de leer 'a == b || x'. – Matthias

Respuesta

10

Sí. Los dobles de Java mantendrán su precisión mejor que su épsilon dado de 0.00001.

Cualquier error de redondeo que se produzca debido al almacenamiento de valores de coma flotante será menor que 0.00001. Uso regularmente 1E-6 o 0.000001 para un épsilon doble en Java sin problemas.

En una nota relacionada, me gusta el formato de epsilon = 1E-5; porque creo que es más legible (1E-5 en Java = 1 x 10^-5). 1E-6 es fácil de distinguir de 1E-5 al leer el código, mientras que 0.00001 y 0.000001 se ven tan similares cuando se mira el código, creo que tienen el mismo valor.

7

Whoa whoa whoa. ¿Hay alguna razón específica por la que está utilizando el punto flotante como moneda, o las cosas mejorarían con un arbitrary-precision, fixed-point number format? No tengo idea de cuál es el problema específico que estás tratando de resolver, pero debes pensar si medio centavo es realmente algo con lo que deseas trabajar, o si es solo un artefacto de usar un formato de números imprecisos.

90

NO utiliza el doble para representar el dinero. Jamas. Use java.math.BigDecimal en su lugar.

A continuación, puede especificar cómo hacer el redondeo (que a veces es dictado por la ley en aplicaciones financieras) y no tiene que hacer tonterías estúpidas como esta cosa épsilon.

En serio, usar tipos de coma flotante para representar el dinero es extremadamente poco profesional.

+54

+1 porque de hecho nunca usas números de coma flotante para representar dinero sino -1 (así que no modifiqué tu conteo) porque usar un épsilon no es un "truco estúpido". Es algo fundamental en la informática científica, no un "truco estúpido". El artículo de Goldberg sobre el tema concuerda con eso. – SyntaxT3rr0r

+47

En serio, no debes asumir que solo porque así es como haces las cosas es la mejor manera en todos los casos. Después de haber trabajado en cuatro bancos diferentes, nunca había visto un sistema de comercio que utilizara BigDecimal, ni recomendaría su uso. –

+2

Peter, ¿qué recomendarías por dinero? Mi preferencia sería un Largo. combinación de base corta para una clase Money. Sin embargo, estoy muy indeciso sobre la situación. Lo he hecho antes ... pero no es algo que pueda probar que funcione. – monksy

1

Los números flotantes solo tienen tantos dígitos significativos, pero pueden aumentar mucho más. Si su aplicación manejará números grandes, notará que el valor épsilon debe ser diferente.

0,001 + 0,001 = 0,002 PERO 12.345.678.900.000.000.000.000 + 1 = 12.345.678.900.000.000.000.000 si está utilizando de punto flotante y doble. No es una buena representación de dinero, a menos que esté seguro de que nunca manejará más de un millón de dólares en este sistema.

+0

El punto flotante no representa valores como 0.1 con precisión ya que internamente almacena el valor como 2^exponente * (1 + fracción). Incluso dentro de un rango razonable como 0.001 + 0.001. Ejecute "print int (1.13 * 100.0)/100.0" si tiene perl. Devuelve 1.12. –

1

Centavos? Si está calculando valores monetarios, realmente no debería usar valores float. El dinero es en realidad valores contables. Los centavos o peniques, etc. podrían considerarse los dos (o lo que sea) dígitos menos significativos de un entero. Puede almacenar y calcular valores monetarios como enteros y dividir entre 100 (por ejemplo, colocar punto o coma dos antes de los dos últimos dígitos). El uso de flotación de puede dar lugar a errores de redondeo extrañas ...

De todos modos, si su épsilon se supone para definir la precisión, se ve un poco demasiado pequeña (demasiado precisa) ...

5

Si se trata de dinero Sugiero verificar el patrón de diseño de Money (originalmente de Martin Fowler's book on enterprise architectural design).

Sugiero la lectura de este enlace para la motivación: http://wiki.moredesignpatterns.com/space/Value+Object+Motivation+v2

+2

Parece que el servidor moredesignpatterns se ha ido y no ha sido reemplazado. Sin embargo, el artículo está en archive.org: http://web.archive.org/web/20090105214308/http://wiki.moredesignpatterns.com/space/Value%2BObject%2BMotivation%2Bv2 –

2

Aunque estoy de acuerdo con la idea de que el doble es malo para el dinero, siendo la idea de dobles comparando tiene interés. En particular, el uso sugerido de épsilon solo se adapta a los números en un rango específico.Aquí hay un uso más general de un epsilon, relativo a la relación de los dos números (prueba de 0 omitido):

booleano igual (doble d1, doble d2) { doble d = d1/d2; return (Math.abs (d - 1.0) < 0.001); }

+1

Eso es muy peligroso debido a cero división. – lethalman

+0

De hecho, '0.000001' y' 0' no serían iguales con este código. – Joey

4

Si puede usar BigDecimal, entonces usarlo, más:

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

¿No debería ser Double.compare (Math.abs (a-b), Math.pow (10, -precision))? – Michael

0

Como señalaron correctamente otros comentaristas, debe Nunca uso aritmética de punto flotante cuando se requieren valores exactos, como por valores monetarios. La razón principal es de hecho el comportamiento de redondeo inherente a los puntos flotantes, pero no olvidemos que lidiar con puntos flotantes significa también tener que lidiar con valores infinitos y NaN.

Como una ilustración de que su enfoque simplemente no funciona, aquí hay un código de prueba simple. Simplemente añadir su EPSILON a 10.0 y mira si el resultado es igual a 10.0 - lo cual no debería ser, ya que la diferencia es evidente que no menos 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); 
    } 

Sorpresa:

ERROR: 10.0 == 10.00001 

Los errores se producen debido a la pérdida si bits significativos en la resta si dos valores de coma flotante tienen exponentes diferentes.

Si se piensa en la aplicación de un enfoque más avanzado "diferencia relativa" según lo sugerido por otros comentaristas, debería leer el excelente artículo de Bruce Dawson Comparing Floating Point Numbers, 2012 Edition, lo que demuestra que este enfoque tiene limitaciones similares y que en realidad hay sin fail comparación aproximada de coma flotante segura que funciona para todos los rangos de números de coma flotante.

Para abreviar: abstenerse de double s para valores monetarios, y utilizar representaciones de números exactos como BigDecimal. En aras de la eficiencia, también podría usar longs interpretado como "millis" (décimas de centavo), siempre que evite de manera confiable los excesos y subdesbordamientos. Esto produce valores máximos representables de 9'223'372'036'854'775.807, que deberían ser suficientes para la mayoría de las aplicaciones del mundo real.