Aritmética Binaria y Decimal

Por Qué Nunca Usar float y double para Representar Montos de Dinero

Cuando aprendemos a programar una de las primeras lecciones es que se usan diferentes tipos de datos para representar números enteros y números no enteros. Una variable entera no puede tener el valor 0,5. En Pascal se usan los tipos Integer y Real y en C están por ejemplo los tipos int, long, float y double.

Supongamos que tenemos que hacer un sistema de permita administrar sumas de dinero; más específicamente, deudas y pagos. Puede ser para un banco, una tarjeta de crédito, un almacén o cualquier aplicación comercial. Para dicho sistema tenemos que hacer una función en C muy sencilla que reciba la deuda actual y un arreglo de pagos que realizó el deudor y que devuelva la deuda actualizada una vez descontados los pagos. Este podría ser el prototipo:

double actualizarDeuda(double deuda, double pagos[], int cantPagos);

Como la deuda puede ser con centavos, no podemos usar int para representarla entonces usamos double.

Lo que hace la función es restar de la deuda cada elemento del arreglo pagos y devolver el monto actualizado.

double actualizarDeuda(double deuda, double pagos[], int cantPagos){
    double deudaActualizada = deuda;
    int i;
    for(i = 0; i<cantPagos;i++){
        deudaActualizada -= pagos[i];
    }
    return deudaActualizada;
}

Vamos a constatar que la función hace lo esperado con una prueba.

Supongamos que un deudor tenía una deuda de $308,21 y realizó 4 pagos por $8,01; $0,20; $299,99 y $0,01. Si bien no es sencillo pagar en efectivo montos como $8,01, es un caso que puede darse perfectamente en transacciones electrónicas.

double deuda = 308.21d;
double pagos[4] = {8.01d, 0.20d, 299.99d, 0.01d};

En teoría la deuda está saldada así que la función debería devolvernos que la deuda es $0,00.

double miDeuda = actualizarDeuda(deuda, pagos, 4);
if(miDeuda == 0.0d){
    printf("Libre deuda.\n");
} else {
    printf("Al Veraz.\n");
}

Sin embargo la prueba imprime Al Veraz. es decir que nuestro sistema sigue registrando una deuda a pesar de que está saldada.

La nueva deuda no es $0,00, pero casi. El valor es $-0,00000000000000909516. Es decir, nuestra función nos calculó que el deudor pagó 9 milbillonésimas de peso más de lo que debía.

Esto nos hace pensar que hay un error de redondeo así que decidimos usar la función round.

miDeuda = round(actualizarDeuda(deuda, pagos, 4));
if(miDeuda == 0.0d){
    printf("Redondeando, logro el libre deuda.\n");
} else {
    printf("Redondeando, igual voy al Veraz.\n");
}

Ahora sí obtenemos el resultado Redondeando, logro el libre deuda lo que es en principio correcto.

A pesar de que en muchos sistemas el problema de la precisión está “solucionado” de esa manera, no se está atacando la causa del problema sino una de sus consecuencias. Como se observa a continuación, redondear el resultado puede llevarnos a dar por saldada una deuda que todavía no está totalmente paga.

pagos[2]-=0.4d;
miDeuda = round(actualizarDeuda(deuda, pagos, 4));
if(miDeuda == 0.0d){
    printf("Con redondeo, debiendo $0,40 igual logro el libre deuda.\n");
} else {
    printf("A pesar de redondear, como debo voy al Veraz.\n");
}

Si descontamos $0,40 de uno de los pagos, está claro que no deberíamos extender el libre deuda. Sin embargo, obtenemos el cartel Con redondeo, debiendo $0,40 igual logro el libre deuda lo que es incorrecto y le hace perder dinero quien nos encargó hacer el sistema. Es una situación complicada para nosotros ya que desde el punto de vista del acreedor, es una atrocidad.

Este problema lo tenemos en cualquier lenguaje de programación y también lo tenemos en muchas aplicaciones muy usadas para hacer cálculos con montos de dinero.

arit-excelLa única solución que ofrecen al problema en el caso de esa planilla de cálculo tan famosa es “redondear” (http://support.microsoft.com/kb/214118/es).

La causa del problema está en la representación binaria de los números de coma flotante. Como no todos los números son representables se tiene que buscar el número más cercano al especificado y eso implica un error en la representación. Por ejemplo el número 0,11 no es representable como double (porque en binario es periódico) y es aproximado con 0,11000000000000000056.

Los números de coma flotante están definidos por el estándar IEEE-754 que data de 1985. A partir de 2008, el estándar incorporó además de los números de binarios de coma flotante que ya existían (que usan base 2 como los float y double) nuevos números decimales de coma flotante (que usan base 10).

Esa es la solución al problema de la precisión que estamos teniendo. Vamos a reescribir la función de esta forma:

_Decimal64 actualizarDeuda(_Decimal64 deuda, _Decimal64 pagos[],int cantPagos){
    _Decimal64 deudaActualizada = deuda;
    int i;
    for(i = 0; i<cantPagos;i++){
        deudaActualizada -= pagos[i];
    }
    return deudaActualizada;
}

Usamos el tipo _Decimal64 en lugar de double. El resto es igual.

Y ahora sin aplicar ningún redondeo, las cuentas simplemente dan el resultado esperado.

_Decimal64 deuda = 308.21dd; 
/* el sufijo dd es el que corresponde al tipo _Decimal64 */
_Decimal64 pagos[4] = {8.01dd, 0.20dd, 299.99dd, 0.01dd};
_Decimal64 miDeuda = actualizarDeuda(deuda, pagos, 4);
if(miDeuda == 0.0dd){
    printf("Con _Decimal64, obtengo en libre deuda normalmente.\n");
} else {
    printf("Con _Decimal64, al Veraz.\n");
}

El soporte de decimales de coma flotante está disponible a partir de GCC 4.2, pero no está disponible para todas las plataformas. En particular en Windows con Cygwin obtenemos un error que dice “error: decimal floating point not supported for this target”.

Otros compiladores sí lo soportan en Windows como por ejemplo el de Intel. http://software.intel.com/en-us/articles/using-decimal-floating-point-with-intel-c-compiler/.

Más información

  1. http://en.wikipedia.org/wiki/IEEE_754-2008
  2. http://speleotrove.com/decimal/

Acceder a Properties Según el Contexto con Spring y Tomcat

Cuando una aplicación web se instala en diferentes servidores suele ser necesario modificar ciertos parámetros como direcciones de otros servidores (por ejemplo de correo), parámetros de conexión a la base de datos o hasta parámetros internos de la aplicación como cantidad de usuarios admitidos o nivel de log.

En un clarísimo artículo intitulado 6 Tips for Managing Property Files with Spring ese es uno de los temas: tener diferentes valores para ciertas properties según en qué ambiente o contexto está ejecutándose la aplicación. Una de las posibles soluciones es tener varios archivos llamados por ejemplo

  • database-prod.properties
  • database-test.properties
  • database-desa.properties

Cada archivo tendrá las mismas properties con los valores acordes a cada ambiente (producción, desarrollo y testing en el ejemplo). Lo que tenemos que hacer es que se lea el archivo correcto según el ambiente.

La clave es Spring que tiene un mecanismo muy flexible para acceder a properties mediante el PropertyPlaceholderConfigurer o el PropertySourcesPlaceholderConfigurer. Esos beans hacen el trabajo pesado; nosotros sólo tenemos que configurarlos según nuestras necesidades.

Una configuración típica de ese bean es algo así

 <context:property-placeholder
        location="classpath:database.properties"
        system-properties-mode="OVERRIDE"
        ignore-unresolvable="true"/>

Teniendo muchas versiones del archivo database.properties, la configuración quedaría de esta forma

 <context:property-placeholder
        location="classpath:database-${ambiente}.properties"
        system-properties-mode="OVERRIDE"
        ignore-unresolvable="true"/>

La intención es que ${ambiente} se reemplace por «desa», «test» o «prod». Para lograrlo hay que hacer estos pasos:

1) Definir en el archivo server.xml del Tomcat una variable de entorno dentro de GlobalNamingResources

  <Environment
            name="ambiente"
            value="desa"
            type="java.lang.String"
            override="false"/>

2) Definir en el elemento Context en el archivo META-INF/context.xml de la aplicación web esta referencia a la variable que creamos en el punto anterior

  <ResourceLink
      global="ambiente"
      name="ambiente"
      type="java.lang.String"/>

De esa forma Spring va a leer el archivo properties según el valor definido en el server.xml.