2008-10-07 31 views
69

Parece que la sustracción está desencadenando algún tipo de problema y el valor resultante es incorrecto.Cómo resolver un problema Java Rounding Double

double tempCommission = targetPremium.doubleValue()*rate.doubleValue()/100d; 

78,75 = 787,5 * 10,0/100d

double netToCompany = targetPremium.doubleValue() - tempCommission; 

708,75 = 787,5 - 78,75

double dCommission = request.getPremium().doubleValue() - netToCompany; 

877,8499999999999 = 1586,6 - 708,75

El valor esperado resultante sería 877,85.

¿Qué se debe hacer para garantizar el cálculo correcto?

Respuesta

84

Para controlar la precisión de la aritmética de coma flotante, debe usar java.math.BigDecimal. Lea The need for BigDecimal de John Zukowski para obtener más información.

Dado su ejemplo, la última línea sería la siguiente usando BigDecimal.

import java.math.BigDecimal; 

BigDecimal premium = BigDecimal.valueOf(1586.6d); 
BigDecimal netToCompany = BigDecimal.valueOf(708.75d); 
BigDecimal commission = premium.subtract(netToCompany); 
System.out.println(commission + " = " + premium + " - " + netToCompany); 

Esto produce la siguiente salida.

877.85 = 1586.6 - 708.75 
+7

No hay manera de que pueda "evitar" por completo los errores aritméticos de coma flotante. La cantidad de bits utilizados para representar un número siempre será finita. Todo lo que puede hacer es usar tipos de datos con mayor precisión (bits). –

+0

Eso es verdad. Editaré mi respuesta para reflejar con mayor precisión el uso de BigDecimal. –

+4

Agregaré una nota que BigDecimal _division_ necesita tratarse de forma un poco diferente que +, -, * ya que de forma predeterminada lanzará una excepción si no puede devolver un valor exacto (1/3, por ejemplo). En una situación similar utilicé: BigDecimal.valueOf (a) .divide (BigDecimal.valueOf (b), 25, RoundingMode.HALF_UP) .doubleValue(), donde 25 si los dígitos máximos de precisión (más que los necesarios para el resultado doble)) –

3

Ver respuestas a this question. Esencialmente, lo que estás viendo es una consecuencia natural del uso de la aritmética de coma flotante.

¿Podría elegir algo de precisión arbitraria (dígitos significativos de sus entradas?) Y redondear su resultado, si se siente cómodo haciéndolo.

6

Cada vez que hace cálculos con dobles, esto puede suceder. Este código le daría 877.85:

doble respuesta = Math.round (dCommission * 100000)/100000.0;

+1

Better brecha que por 100000.0 en lugar de sólo 100.000; Math.round devuelve un valor largo, por lo que usarás la división entera de lo contrario. –

4

Ahorre la cantidad de centavos en lugar de dólares, y simplemente haga el formato a dólares cuando lo muestre. De esta forma, puede usar un número entero que no tenga problemas de precisión.

+0

La única pega es que una fracción de un centavo se puede perder al principio del proceso; esto podría ser malo para las aplicaciones financieras. Si esto es un problema, puede guardar la cantidad de 1/10 centavos o la precisión que necesite. –

+0

Si pudiera retroceder en el tiempo y darle a mi pasado un "truco", este sería el lugar. Precio como centavos! (o 1e-3 o 1e-6 - que todavía es bueno para 2+ millones de dólares como int) – Trenton

10

Otro ejemplo:

double d = 0; 
for (int i = 1; i <= 10; i++) { 
    d += 0.1; 
} 
System.out.println(d); // prints 0.9999999999999999 not 1.0 

Uso BigDecimal lugar.

EDIT:

Además, sólo señalar que esto no es un problema de redondeo 'Java'. Otros lenguajes muestran comportamiento similar (aunque no necesariamente consistente). Java al menos garantiza un comportamiento consistente en este sentido.

53

Como las respuestas anteriores indicaron, esto es una consecuencia de hacer aritmética de coma flotante.

Como se sugirió en un póster anterior, Cuando haga cálculos numéricos, use java.math.BigDecimal.

Sin embargo, hay un problema con el uso de BigDecimal.Cuando se convierte desde el valor doble a BigDecimal, tiene la opción de utilizar un nuevo constructor BigDecimal(double) o el método de fábrica estático BigDecimal.valueOf(double). Use el método de fábrica estático.

La doble constructor convierte toda la precisión de la double a un BigDecimal mientras que la fábrica estática convierte efectivamente a un String, a continuación, convierte eso a un BigDecimal.

Esto se vuelve relevante cuando se encuentra con estos sutiles errores de redondeo. Un número puede mostrar como .585, pero internamente su valor es '0.58499999999999996447286321199499070644378662109375'. Si utilizó el constructor BigDecimal, obtendría el número que NO es igual a 0.585, mientras que el método estático le daría un valor igual a 0.585.

 
double value = 0.585; 
System.out.println(new BigDecimal(value)); 
System.out.println(BigDecimal.valueOf(value)); 

en mi sistema da

 
0.58499999999999996447286321199499070644378662109375 
0.585 
+3

¡Me he topado con este problema muchas veces y es realmente bastante molesto! – Richard

+1

días de ahorro. gracias! –

+3

Tenga en cuenta que la razón es que el método estático utiliza un MathContext ILIMITADO que significa prácticamente: nuevo BigDecimal (valor, nuevo MathContext (0, RoundingMode.HALF_UP));) –

7

que modificaría el ejemplo anterior de la siguiente manera:

import java.math.BigDecimal; 

BigDecimal premium = new BigDecimal("1586.6"); 
BigDecimal netToCompany = new BigDecimal("708.75"); 
BigDecimal commission = premium.subtract(netToCompany); 
System.out.println(commission + " = " + premium + " - " + netToCompany); 

De esta manera se evitan los peligros de usar cuerdas para empezar. Otra alternativa:

import java.math.BigDecimal; 

BigDecimal premium = BigDecimal.valueOf(158660, 2); 
BigDecimal netToCompany = BigDecimal.valueOf(70875, 2); 
BigDecimal commission = premium.subtract(netToCompany); 
System.out.println(commission + " = " + premium + " - " + netToCompany); 

creo que estas opciones son mejores que el uso de dobles. En webapps, los números comienzan de todas formas.

1

Hasta ahora la forma más elegante y más eficiente de hacerlo en Java:

double newNum = Math.floor(num * 100 + 0.5)/100; 
0
double rounded = Math.rint(toround * 100)/100; 
-1

Aunque no se debe utilizar dobles para cálculos precisos el siguiente truco me ha ayudado si redondear los resultados de todas formas .

public static int round(Double i) { 
    return (int) Math.round(i + ((i > 0.0) ? 0.00000001 : -0.00000001)); 
} 

Ejemplo:

Double foo = 0.0; 
    for (int i = 1; i <= 150; i++) { 
     foo += 0.00010; 
    } 
    System.out.println(foo); 
    System.out.println(Math.round(foo * 100.0)/100.0); 
    System.out.println(round(foo*100.0)/100.0); 

que imprime:

0.014999999999999965 
0.01 
0.02 

Más información: http://en.wikipedia.org/wiki/Double_precision

+0

Dado que .5 * puede * ser representado exactamente como un binario fracción, el único caso donde esto tendría un efecto es cuando el valor es legítimamente diferente. –

+0

@Michael: mi ejemplo fue defectuoso. Sin embargo, el problema persiste. Por lo tanto, agregué un ejemplo. – Timon

+3

El problema persiste es que su método devolverá el resultado incorrecto cuando el valor realmente * es * algo así como 0.01499999999 - es fundamentalmente la manera incorrecta de abordar problemas como este. –

-3

Es muy sencillo.

Utilice el operador% .2f para la salida. ¡Problema resuelto!

Por ejemplo:

int a = 877.8499999999999; 
System.out.printf("Formatted Output is: %.2f", a); 

el código anterior da como resultado una salida de impresión de: 877,85

El% operador .2f define que sólo dos cifras decimales deben ser utilizados.

+0

¡Esto NO soluciona problemas de aproximación y aproximación de mantisse! ¡Lo que ves no es en lo que trabajas! – Benj

+0

Si redondeas a 1 decimal, obtendrás 877.8. Lo cual probablemente no es lo que quieres. – mortensi

3

Este es un problema divertido.

La idea detrás de la respuesta de Timons es que especifique un épsilon que representa la precisión más pequeña que un doble legal puede ser. Si sabe en su aplicación que nunca necesitará precisión por debajo de 0.00000001 entonces, lo que sugiere es suficiente para obtener un resultado más preciso muy cerca de la verdad. Útil en aplicaciones donde conocen por adelantado su máxima precisión (por ejemplo en finanzas para precisiones de moneda, etc.)

Sin embargo, el problema fundamental al tratar de redondearlo es que cuando se divide por un factor para reescalarlo, en realidad se introduce otra posibilidad para problemas de precisión. Cualquier manipulación de dobles puede introducir problemas de imprecisión con frecuencia variable. Especialmente si usted está tratando de ronda en un dígito significativo (por lo que sus operandos son < 0), por ejemplo, si se ejecuta el siguiente código con Timons:

System.out.println(round((1515476.0) * 0.00001)/0.00001); 

resultará en 1499999.9999999998 donde el objetivo aquí es redondear en las unidades de 500000 (es decir, queremos 1500000)

De hecho, la única manera de estar completamente seguro de haber eliminado la imprecisión es pasar por un BigDecimal para escalar. p.ej.

System.out.println(BigDecimal.valueOf(1515476.0).setScale(-5, RoundingMode.HALF_UP).doubleValue()); 

Usando una mezcla de la estrategia épsilon y la estrategia de BigDecimal le dará un control preciso sobre su precisión. La idea es que el épsilon te acerca mucho y luego el BigDecimal eliminará cualquier imprecisión causada al volver a escalar después. Aunque usar BigDecimal reducirá el rendimiento esperado de su aplicación.

Se me ha indicado que el paso final de usar BigDecimal para reescalarlo no siempre es necesario en algunos casos de uso cuando puede determinar que no hay valor de entrada que la división final puede reintroducir un error. Actualmente no sé cómo determinarlo adecuadamente, así que si alguien sabe cómo, me gustaría saberlo.

2

Mejor aún utilizar JScience como BigDecimal es bastante limitado (por ejemplo, ninguna función sqrt)

double dCommission = 1586.6 - 708.75; 
System.out.println(dCommission); 
> 877.8499999999999 

Real dCommissionR = Real.valueOf(1586.6 - 708.75); 
System.out.println(dCommissionR); 
> 877.850000000000 
+0

¡Gracias por la lib! ¿Pero no está un poco sobrecargado por algo simple? – Benj

+0

@Benj, podrías decir lo mismo sobre la guayaba;) – Tomasz

Cuestiones relacionadas