¿Por qué no utilizar Double o Float para representar moneda?
Siempre me han dicho que nunca represente dinero con double
o float
tipos, y esta vez les planteo la pregunta: ¿por qué?
Estoy seguro de que hay una muy buena razón, simplemente no sé cuál es.
Porque los flotantes y los dobles no pueden representar con precisión los múltiplos de base 10 que usamos para el dinero. Este problema no es sólo para Java, es para cualquier lenguaje de programación que utilice tipos de punto flotante base 2.
En base 10, puedes escribir 10,25 como 1025 * 10 -2 (un número entero multiplicado por una potencia de 10). Los números de punto flotante IEEE-754 son diferentes, pero una forma muy sencilla de pensar en ellos es multiplicarlos por una potencia de dos. Por ejemplo, podrías estar considerando 164 * 2 -4 (un número entero multiplicado por una potencia de dos), que también es igual a 10,25. No es así como se representan los números en la memoria, pero las implicaciones matemáticas son las mismas.
Incluso en base 10, esta notación no puede representar con precisión la mayoría de las fracciones simples. Por ejemplo, no puedes representar 1/3: la representación decimal se repite (0,3333...), por lo que no existe un número entero finito que puedas multiplicar por una potencia de 10 para obtener 1/3. Podrías conformarte con una secuencia larga de 3 y un exponente pequeño, como 333333333 * 10 -10 , pero no es exacto: si multiplicas eso por 3, no obtendrás 1.
Sin embargo, para contar dinero, al menos para los países cuyo dinero está valorado dentro de un orden de magnitud del dólar estadounidense, normalmente todo lo que se necesita es poder almacenar múltiplos de 10 -2 , por lo que realmente no importa. ese 1/3 no se puede representar.
El problema con los flotantes y los dobles es que la gran mayoría de los números parecidos al dinero no tienen una representación exacta como un número entero multiplicado por una potencia de 2. De hecho, los únicos múltiplos de 0,01 entre 0 y 1 (que son significativos al tratar con dinero porque son centavos enteros) que se pueden representar exactamente como un número binario de punto flotante IEEE-754 son 0, 0,25, 0,5, 0,75 y 1. Todos los demás están desviados por una pequeña cantidad. Como analogía con el ejemplo de 0,333333, si tomas el valor de punto flotante de 0,01 y lo multiplicas por 10, no obtendrás 0,1. En su lugar obtendrá algo como 0.099999999786...
Representar el dinero como un double
o float
probablemente se verá bien al principio a medida que el software redondea los pequeños errores, pero a medida que realiza más sumas, restas, multiplicaciones y divisiones en números inexactos, los errores se agravarán y terminará con valores que son visiblemente no es correcto. Esto hace que los flotadores y los dobles sean inadecuados para manejar dinero, donde se requiere una precisión perfecta para múltiplos de potencias de base 10.
Una solución que funciona en casi cualquier idioma es utilizar números enteros y contar centavos. Por ejemplo, 1025 sería $10,25. Varios idiomas también tienen tipos integrados para tratar con dinero. Entre otros, Java tiene la BigDecimal
clase, Rust tiene la rust_decimal
caja y C# tiene el decimal
tipo.
De Bloch, J., Effective Java, (2.ª ed., artículo 48. 3.ª ed., artículo 60):
Los tipos
float
ydouble
son particularmente inadecuados para cálculos monetarios porque es imposible representar 0,1 (o cualquier otra potencia negativa de diez) comofloat
odouble
exactamente.Por ejemplo, supongamos que tienes $1,03 y gastas 42c. ¿Cuánto dinero te queda?
System.out.println(1.03 - .42);
imprime
0.6100000000000001
.La forma correcta de resolver este problema es utilizar
BigDecimal
,int
olong
para cálculos monetarios.
Aunque BigDecimal
tiene algunas advertencias (consulte la respuesta actualmente aceptada).
Esta no es una cuestión de exactitud ni de precisión. Se trata de satisfacer las expectativas de los seres humanos que utilizan la base 10 para los cálculos en lugar de la base 2. Por ejemplo, utilizar dobles para los cálculos financieros no produce respuestas que sean "incorrectas" en un sentido matemático, pero puede producir respuestas que sean No es lo que se espera en un sentido financiero.
Incluso si redondea sus resultados en el último minuto antes de la salida, ocasionalmente puede obtener un resultado usando dobles que no coincida con las expectativas.
Usando una calculadora, o calculando los resultados a mano, 1,40 * 165 = 231 exactamente. Sin embargo, al usar dobles internamente, en mi entorno de compilador/sistema operativo, se almacena como un número binario cercano a 230.99999... por lo que si trunca el número, obtendrá 230 en lugar de 231. Puede razonar que redondear en lugar de truncar sería han dado el resultado deseado de 231. Eso es cierto, pero el redondeo siempre implica truncamiento. Cualquiera que sea la técnica de redondeo que utilice, todavía existen condiciones límite como ésta que se redondearán hacia abajo cuando usted espera que se redondeen hacia arriba. Son lo suficientemente raros como para que a menudo no se encuentren mediante pruebas u observación casuales. Es posible que tenga que escribir código para buscar ejemplos que ilustren resultados que no se comportan como se esperaba.
Suponga que desea redondear algo al centavo más cercano. Entonces tomas el resultado final, multiplicas por 100, sumas 0,5, truncas y luego divides el resultado por 100 para volver a los centavos. Si el número interno que almacenó fue 3.46499999.... en lugar de 3.465, obtendrá 3.46 en lugar de 3.47 cuando redondee el número al centavo más cercano. Pero sus cálculos de base 10 pueden haber indicado que la respuesta debería ser exactamente 3,465, lo que claramente debería redondearse hacia arriba a 3,47, no hacia abajo a 3,46. Este tipo de cosas suceden ocasionalmente en la vida real cuando se utilizan dobles para cálculos financieros. Es raro, por lo que a menudo pasa desapercibido como un problema, pero sucede.
Si utiliza base 10 para sus cálculos internos en lugar de dobles, las respuestas siempre son exactamente las que esperan los humanos, suponiendo que no haya otros errores en su código.
Algunas de estas respuestas me preocupan. Creo que los dobles y las flotaciones tienen un lugar en los cálculos financieros. Ciertamente, al sumar y restar cantidades monetarias no fraccionarias no habrá pérdida de precisión al utilizar clases de números enteros o clases BigDecimal. Pero cuando se realizan operaciones más complejas, a menudo se obtienen resultados con varios o muchos decimales, sin importar cómo se almacenen los números. El problema es cómo presentas el resultado.
Si su resultado está en el límite entre redondear hacia arriba y hacia abajo, y ese último centavo realmente importa, probablemente debería decirle al espectador que la respuesta está casi en el medio, mostrando más decimales.
El problema de los dobles, y más aún de los flotantes, es cuando se utilizan para combinar números grandes y pequeños. En Java,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
resultados en
1.1875