Posted on 10-08-2007
Filed Under (Java, Programación) by galifate

Pues nada, traduciendo chuminadas. Aquí lo que tenemos es un articulito de CoreJavaTechTips en el que, para aquellos que estamos liados con el java, se recomienda el uso de la clase BigDecimal para representar cantidades de dinerito, en lugar de usar un tipo primitivo como double. Esta tonteria puede ser útil para aquellos que empiezan en el mundillo, aunque para los que ya llevamos un tiempo está un poco pasado. De todas formas sirve para rellenar.

El artículo está hecho por un tal John Zukowski y dice algo así…

Si curras con número en coma flotante puedes usar tipos primitivos como el double, pero si tienes que aplicarle operaciones como si a un importe le añadieras el IVA, por ejemplo, ten cuidadito. Éste tipo de operaciones puede que no te den el resultado correcto, pues sólo te puede dar el valor que se pueda almacenar en un número binario y tal. A continuación nos dan un ejemplo.

Tenemos un programa “Calc” con el que empezamos con un montante de 100.05$, y nos dan un 10% de descuento antes de aplicarnos un 5% de impuestos los jodíos. Para ver el resultado, el pogramita usa la clase NumberFormat para formatear el resultado para mostrar el dinerito.

import java.text.NumberFormat;

public class Calc {
  public static void main(String args[]) {
    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));
  }
}

Como usamos el tipo double para todos los cálculos internos, tenemos los siguientes resultados:

Subtotal : $100.05
Discount : $10.00
Total : $90.04
Tax : $4.50
Tax+Total: $94.55

El valor Total es lo que se espera, pero Tax+Total parece que es más de lo que toca(¡¡¡malditos impuestos!!!). Para que el Total nos dé lo que nos da(90.04$), el descuento debería ser de 10.01$ y no 10.00$. O sea, que perdemos 0.01$ por errores de redondeo. Aquí están los valores sin formatear:

Subtotal : 100.05
Discount : 10.005
Total : 90.045
Tax : 4.50225
Tax+Total: 94.54725

¿Por qué el 90.045 se redondea a 90.04 y no a 90.05, como tiene que ser? Pues por RoundingMode, un enumeration de Java SE 6 que controla el redondeo. El NumberFormat para las cantidades monetarias está en modo HALF_EVEN por defecto, con lo que si el valor a redondear es equidistante se redondea hacia el valor par (4) y no al valor correcto en este caso (5).

Los otros modos de RoundingMode:

  • CEILING redondea hacia arriba
  • DOWN redondea hacia el zero
  • FLOOR redondea hacia abajo
  • UP redondea alejándose del zero
  • HALF_DOWN redondea al más cercano, per en caso de equidistancia se va hacia abajo
  • HALF_UP igual que el anterior, pero en caso de equidistancia se va para arriba
  • UNNECESSARY sin redondeo

Antes de intentar corregir el tema, vamos a hacer otra prueba pero con 0.70$ y sin descuentos.

Total : $0.70
Tax : $0.03
Tax+Total: $0.74

En este caso no es sólo un error de redondeo. Vamos a ver los valores sin formatear:

Total : 0.7
Tax : 0.034999999999999996
Tax+Total: 0.735

El valor de los impuestos simplemente no se puede almacenar en un double. No tiene representación en binario.

La clase BigDecimal ayuda a resolver problemas como estos. BigDecimal guarda números en coma flotante con una precisión prácticamente ilimitada. Y para manipular el dato hay que usar las operaciones add(value), subtract(value), multiply(value), o divide(value, scale, roundingMode).

Para sacar un valor de un BigDecimal, hay que poner una escala y un modo de redondeo con setScale(scale, roundingMode), o usar los métodos toString() o toPlainString(). El método toString() puede usar notación cientifica mientras que toPlainString() no.

Antes de transformar el programita para usar la clase BigDecimal, hay que ver como se crea un objeto de esos. Hay 16 constructores del ala para la clase de marras. Puesto que es posible no poder guardar el valor de un BigDecimal en un objeto primitivo como un double, es mejor crear un BigDecimal desde un String. Para demostrar este error, aquí tienes un ejemplillo bien simple:

  double dd = .35;
  BigDecimal d = new BigDecimal(dd);
  System.out.println(".35 = " + d);

La salida no es lo que habrías esperado:

  .35 = 0.34999999999999997779553950749686919152736663818359375

En su lugar, lo que hay que hace es crear el BigDecimal con el string “.35″:

  BigDecimal d = new BigDecimal(".35");

lo que nos da la siguiente salida (que mola más):

  .35 = 0.35

Una vez creado el objeto, puedes asignarle una escala y su modo de redondeo con setScale(). Ojo, que el BigDecimal es inmutable, por lo que tendrás que guardar el resultado de la operación:

  d = d.setScale(2, RoundingMode.HALF_UP);

A continuación el programa modificado. Nos aseguramos que las operaciones rulan bien para los dólares estableciendo la correcta escala.

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Calc2 { 

  public static void main(String args[]) {
    BigDecimal amount = new BigDecimal("100.05");
    BigDecimal discountPercent = new BigDecimal("0.10");
    BigDecimal discount = amount.multiply(discountPercent);
    discount = discount.setScale(2, RoundingMode.HALF_UP);
    BigDecimal total = amount.subtract(discount);
    total = total.setScale(2, RoundingMode.HALF_UP);
    BigDecimal taxPercent = new BigDecimal("0.05");
    BigDecimal tax = total.multiply(taxPercent);
    tax = tax.setScale(2, RoundingMode.HALF_UP);
    BigDecimal taxedTotal = total.add(tax);
    taxedTotal = taxedTotal.setScale(2, RoundingMode.HALF_UP);
    System.out.println("Subtotal : " + amount);
    System.out.println("Discount : " + discount);
    System.out.println("Total : " + total);
    System.out.println("Tax : " + tax);
    System.out.println("Tax+Total: " + taxedTotal);
  }
}

Fíjate que no usamos NumberFormat, aunque lo podemos usar si vamos a sacar el símbolo de la moneda.

Ahora los resultados parece que cuadran:

Subtotal : 100.05
Discount : 10.01
Total : 90.04
Tax : 4.50
Tax+Total: 94.54

BigDecimal da mucho más de sí, pero para empezar ya está bien. También tenemos la clase BigInteger para cuando queramos precisión total con números enteros. Si quereis más info, ya sabéis, a la documentación de la API de Java.

Discutir en el foro (0)

    Read More   

Comments

Javi on 18 Abril, 2008 at 4:19 am #

Hola el ejemplo que ponéis sobre BigDecimal y double no es del todo exacto.
Me he fijado que en el BigDecimal estás redondeando en cada operación y en el double no haces lo mismo.
He hecho una prueba y, si redondeas el double con el setScale del BigDecimal, da el mismo resultado.


Post a Comment
Name:
Email:
Website:
Comments: