Supongamos que tenemos una cantidad de dinero tal que 100,05 a la que aplicamos un 10% de descuento y posteriormente un 5% en concepto de impuestos. Las líneas de código que calculan esto son:
import java.text.NumberFormat; double amount = 100.05; double discount = amount * 0.10; double total = amount - discount; double tax = total * 0.05; double taxedTotal = tax + total; NumberFormat money = NumberFormat.getCurrencyInstance(); System.out.println("Subtotal: "+ money.format(amount)); System.out.println("Discount: " + money.format(discount)); System.out.println("Total: " + money.format(total)); System.out.println("Tax: " + money.format(tax)); System.out.println("Tax+Total: " + money.format(taxedTotal));
Y el resultado es:
Subtotal: 100,05 €
Discount: 10,00 €
Total: 90,04 €
Tax: 4,50 €
Tax+Total: 94,55 €
Si nos fijamos en la suma de total más impuestos hay una diferencia de 0.01 y si presentamos un desglose de precios como este a un usuario puede que este piense que hay algún error en el cálculo, le genere desconfianza y no haga la compra en el peor de los casos. Viendo los valores sin los redondeos que hace NumberFormat tenemos:
Subtotal: 100.05
Discount: 10.005
Total: 90.045
Tax: 4.50225
Tax+Total: 94.54725
Los redondeos que hace NumberFormat es HALF_EVEN por defecto, de modo que cuando un decimal está equidistante a las dos partes se redondea a la parte par por lo que con una precisión de dos decimales:
Discount: 10.005 se redondea a 10.00
Total: 90.045 se redondea a 90.04
Tax: 4.50225 se redondea a 4.50
Tax+Total: 94.54725 se redondea a 94.55
En este caso se trata de un problema de redondeo pero ahora supongamos que tenemos una cantidad de 0,70 céntimos a la que no aplicamos un descuento pero si el procentaje de impuestos del 5%. Tendríamos:
import java.text.NumberFormat; double amount = 0.70; double tax = amount * 0.05; double taxedTotal = tax + amount; NumberFormat money = NumberFormat.getCurrencyInstance(); System.out.println("Subtotal: "+ money.format(amount)); System.out.println("Tax: " + money.format(tax)); System.out.println("Tax+Total: " + money.format(taxedTotal));
Subtotal: 0,70 €
Tax: 0,03 €
Tax+Total: 0,74 €
Nos encontramos otra vez con la diferencia de 0.01. Vemos los valores sin redondear por NumberFormat:
Subtotal: 0.7
Tax: 0.034999999999999996
Tax+Total: 0.735
Aquí se ve que el resultado de ciertas operaciones aritméticas entre datos double (o float) son almacenadas por una computadora con errores de precisión, 0.70 * 0.05 (debería ser 0.035).
Para evitar estos errores debemos utilizar la clase BigDecimal que pemite almacenar números con una precisión en la práctica infinita en base 10, realizar los cálculos como los humanos esperan, en base diez, y hacer los redondeos de precisión. Aplicando una precisión de dos decimales a los números y usando BigDecimal tenemos:
import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; RoundingMode RM = RoundingMode.HALF_EVEN; BigDecimal amount = new BigDecimal("100.05"); BigDecimal discountPercent = new BigDecimal("0.10"); BigDecimal discount = amount.multiply(discountPercent).setScale(2, RM); BigDecimal total = amount.subtract(discount).setScale(2, RM); BigDecimal taxPercent = new BigDecimal("0.05"); BigDecimal tax = total.multiply(taxPercent).setScale(2, RM); BigDecimal taxedTotal = total.add(tax).setScale(2, RM); NumberFormat money = NumberFormat.getCurrencyInstance(); System.out.println("Subtotal : " + money.format(amount)); System.out.println("Discount : " + money.format(discount)); System.out.println("Total : " + money.format(total)); System.out.println("Tax : " + money.format(tax)); System.out.println("Tax+Total: " + money.format(taxedTotal));
Ahora los precios si están correctos:
Subtotal : 100,05 €
Discount : 10,00 €
Total : 90,05 €
Tax : 4,50 €
Tax+Total: 94,55 €
Para el otro caso en el que teníamos un error de precisión:
import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; RoundingMode RM = RoundingMode.HALF_EVEN; BigDecimal amount = new BigDecimal("0.70"); BigDecimal taxPercent = new BigDecimal("0.05"); BigDecimal tax = amount.multiply(taxPercent).setScale(2, RM); BigDecimal taxedTotal = tax.add(amount).setScale(2, RM); NumberFormat money = NumberFormat.getCurrencyInstance(); System.out.println("Subtotal: "+ money.format(amount)); System.out.println("Tax: " + money.format(tax)); System.out.println("Tax+Total: " + money.format(taxedTotal));
Subtotal: 0,70 €
Tax: 0,04 €
Tax+Total: 0,74 €
Referencia:
http://www.javamexico.org/blogs/luxspes/por_que_usar_bigdecimal_y_no_double_para_calculos_aritmeticos_financieros
http://speleotrove.com/decimal/decifaq1.html#tzeros
http://www.mkyong.com/java/how-do-calculate-monetary-values-in-java-double-vs-bigdecimal/
http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal
http://stackoverflow.com/questions/7539/please-explain-the-use-of-java-math-mathcontext/7561#7561
http://en.wikipedia.org/wiki/Floating_point