Siempre me han dicho que nunca represente dinero double
o float
tipos, y esta vez le hago la pregunta: ¿por qué?
Estoy seguro de que hay una muy buena razón, simplemente no sé de qué se trata.
floating-point
currency
Fran Fitzpatrick
fuente
fuente
Respuestas:
Debido a que los flotadores y los dobles no pueden representar con precisión los múltiplos de base 10 que usamos para obtener dinero. Este problema no es solo para Java, es para cualquier lenguaje de programación que use tipos de punto flotante base 2.
En la base 10, puede escribir 10.25 como 1025 * 10 -2 (un entero multiplicado por una potencia de 10). Los números de punto flotante IEEE-754 son diferentes, pero una forma muy simple de pensar en ellos es multiplicar por una potencia de dos. Por ejemplo, podría estar mirando 164 * 2 -4 (un número entero multiplicado por una potencia de dos), que también es igual a 10.25. Así no se representan los números en la memoria, pero las implicaciones matemáticas son las mismas.
Incluso en la base 10, esta notación no puede representar con precisión la mayoría de las fracciones simples. Por ejemplo, no puede representar 1/3: la representación decimal se repite (0.3333 ...), por lo que no hay un número entero finito que pueda multiplicar por una potencia de 10 para obtener 1/3. Podría establecerse en una secuencia larga de 3 y un pequeño exponente, como 333333333 * 10-10 , pero no es exacto: si multiplica eso por 3, no obtendrá 1.
Sin embargo, con el fin de contar el dinero, al menos para los países cuyo dinero está valorado dentro de un orden de magnitud del dólar estadounidense, por lo general, todo lo que necesita es poder almacenar múltiplos de 10 -2 , por lo que realmente no importa ese 1/3 no puede ser representado.
El problema con los flotadores y los dobles es que la gran mayoría de los números similares al dinero no tienen una representación exacta como un entero multiplicado por una potencia de 2. De hecho, los únicos múltiplos de 0.01 entre 0 y 1 (que son significativos cuando se trata con dinero porque son centavos enteros) que se pueden representar exactamente como un número de punto flotante binario IEEE-754 son 0, 0.25, 0.5, 0.75 y 1. Todos los demás están apagados por una pequeña cantidad. Como analogía con el ejemplo de 0.333333, si toma el valor de coma flotante para 0.1 y lo multiplica por 10, no obtendrá 1.
Representar el dinero como
double
ofloat
probablemente se verá bien al principio, ya que el software completa 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 visibles no es correcto. Esto hace que los flotadores y los dobles sean inadecuados para tratar con 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 usar enteros y contar centavos. Por ejemplo, 1025 sería $ 10.25. Varios idiomas también tienen tipos incorporados para lidiar con el dinero. Entre otros, Java tiene la
BigDecimal
clase y C # tiene eldecimal
tipo.fuente
1.0 / 10 * 10
puede no ser lo mismo que 1.0.De Bloch, J., Effective Java, 2nd ed, Item 48:
Aunque
BigDecimal
tiene algunas advertencias (consulte la respuesta actualmente aceptada).fuente
long a = 104
y cuentas en centavos en lugar de dólares.BigDecimal
.Esto no es una cuestión de precisión, ni es una cuestión de precisión. Se trata de cumplir con las expectativas de los humanos que usan la base 10 para los cálculos en lugar de la base 2. Por ejemplo, el uso de dobles para los cálculos financieros no produce respuestas que sean "incorrectas" en un sentido matemático, pero puede producir respuestas que son no es lo que se espera en un sentido financiero.
Incluso si completa sus resultados en el último minuto antes de la salida, de vez en cuando puede obtener un resultado utilizando dobles que no coinciden con las expectativas.
Usando una calculadora, o calculando resultados a mano, 1.40 * 165 = 231 exactamente. Sin embargo, internamente usando dobles, en mi entorno de compilador / sistema operativo, se almacena como un número binario cercano a 230.99999 ... así que si trunca el número, obtiene 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. Independientemente de la técnica de redondeo que utilice, todavía hay condiciones de límite como esta que se redondearán hacia abajo cuando espere que se redondee. Son lo suficientemente raros que a menudo no se encontrarán mediante pruebas u observaciones casuales. Es posible que deba escribir algún código para buscar ejemplos que ilustren resultados que no se comporten como se esperaba.
Suponga que quiere redondear algo al centavo más cercano. Entonces toma su resultado final, multiplica por 100, suma 0.5, trunca, luego divide el resultado por 100 para volver a 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 redondea el número al centavo más cercano. Pero sus cálculos de base 10 pueden haber indicado que la respuesta debería ser 3.465 exactamente, lo que claramente debería redondear a 3.47, no a 3.46. Este tipo de cosas suceden ocasionalmente en la vida real cuando usas dobles para cálculos financieros. Es raro, por lo que a menudo pasa desapercibido como un problema, pero sucede.
Si utiliza la base 10 para sus cálculos internos en lugar de dobles, las respuestas son siempre exactamente las que esperan los humanos, suponiendo que no haya otros errores en su código.
fuente
Math.round(0.49999999999999994)
devuelve 1?Me preocupan algunas de estas respuestas. Creo que los dobles y los flotadores 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 usar clases enteras o clases BigDecimal. Pero cuando realiza operaciones más complejas, a menudo termina con resultados que salen varios o muchos lugares decimales, sin importar cómo almacene los números. El problema es cómo presentas el resultado.
Si su resultado está en el límite entre ser redondeado hacia arriba y hacia abajo, y ese último centavo realmente importa, probablemente debería estar diciéndole al espectador que la respuesta está casi en el medio, mostrando más decimales.
El problema con los dobles, y más aún con los flotadores, es cuando se usan para combinar números grandes y números pequeños. En java
resultados en
fuente
Flotadores y dobles son aproximados. Si crea un BigDecimal y pasa un flotador al constructor, verá lo que realmente es el flotador:
probablemente no es así como quieres representar $ 1.01.
El problema es que la especificación IEEE no tiene una forma de representar exactamente todas las fracciones, algunas de ellas terminan como fracciones repetidas, por lo que terminan con errores de aproximación. Dado que a los contadores les gusta que las cosas salgan exactamente al centavo, y los clientes se molestarán si pagan su factura y después de que se procese el pago, deben .01 y se les cobra una tarifa o no pueden cerrar su cuenta, es mejor usar tipos exactos como decimal (en C #) o java.math.BigDecimal en Java.
No es que el error no sea controlable si redondeas: mira este artículo de Peter Lawrey . Es más fácil no tener que redondear en primer lugar. La mayoría de las aplicaciones que manejan dinero no requieren muchas matemáticas, las operaciones consisten en agregar cosas o asignar cantidades a diferentes categorías. La introducción de punto flotante y redondeo solo complica las cosas.
fuente
float
,double
yBigDecimal
representan valores exactos . La conversión de código a objeto es inexacta, así como otras operaciones. Los tipos en sí mismos no son inexactos.Me arriesgaré a ser rechazado, pero creo que la inadecuación de los números de coma flotante para los cálculos de divisas está sobrevalorada. Siempre y cuando se asegure de hacer el redondeo de centavo correctamente y tenga suficientes dígitos significativos para trabajar con el fin de contrarrestar la falta de coincidencia de representación decimal binaria explicada por zneak, no habrá ningún problema.
Las personas que calculan con moneda en Excel siempre han usado flotantes de doble precisión (no hay ningún tipo de moneda en Excel) y todavía no he visto a nadie quejarse de errores de redondeo.
Por supuesto, debes mantenerte dentro de lo razonable; por ejemplo, una tienda virtual sencilla probablemente nunca experimentaría ningún problema con los flotadores de doble precisión, pero si lo hace, por ejemplo, contabilidad o cualquier otra cosa que requiera agregar una gran cantidad (sin restricciones) de números, no querrá tocar números de coma flotante con un pie de diez polo.
fuente
Si bien es cierto que el tipo de coma flotante solo puede representar datos decimales aproximados, también es cierto que si uno redondea los números con la precisión necesaria antes de presentarlos, obtiene el resultado correcto. Generalmente.
Por lo general, porque el tipo doble tiene una precisión inferior a 16 cifras. Si necesita una mayor precisión, no es un tipo adecuado. También se pueden acumular aproximaciones.
Debe decirse que incluso si usa la aritmética de punto fijo, todavía tiene que redondear números, si no fuera por el hecho de que BigInteger y BigDecimal dan errores si obtiene números decimales periódicos. Entonces hay una aproximación también aquí.
Por ejemplo, COBOL, históricamente utilizado para cálculos financieros, tiene una precisión máxima de 18 cifras. Por lo tanto, a menudo hay un redondeo implícito.
En conclusión, en mi opinión, el doble no es adecuado principalmente por su precisión de 16 dígitos, que puede ser insuficiente, no porque sea aproximado.
Considere la siguiente salida del programa posterior. Muestra que después del redondeo doble da el mismo resultado que BigDecimal hasta la precisión 16.
fuente
El resultado del número de coma flotante no es exacto, lo que los hace inadecuados para cualquier cálculo financiero que requiera un resultado exacto y no una aproximación. float y double están diseñados para cálculos de ingeniería y científicos y muchas veces no producen resultados exactos, también el resultado del cálculo de coma flotante puede variar de JVM a JVM. Mire el siguiente ejemplo de BigDecimal y la primitiva doble que se usa para representar el valor monetario, es bastante claro que el cálculo de coma flotante puede no ser exacto y uno debe usar BigDecimal para los cálculos financieros.
Salida:
fuente
double
FP binario para el centavo no tendría problemas para calcular el 0.5 centavo como tampoco lo haría el FP decimal. Si los cálculos de punto flotante arrojan un valor de interés de, por ejemplo, 123.499941 ¢, ya sea a través de FP binario o FP decimal, el problema de doble redondeo es el mismo: ninguna ventaja de ninguna manera. Su premisa parece suponer que el valor matemáticamente preciso y el FP decimal son los mismos, algo que incluso el FP decimal no garantiza. 0.5 / 7.0 * 7.0 es un problema para FP binaria y deicmal. IAC, la mayoría será discutible ya que espero que la próxima versión de C proporcione FP decimal.Como se dijo anteriormente "Representar el dinero como doble o flotante probablemente se verá bien al principio, ya que el software completa los pequeños errores, pero a medida que realiza más sumas, restas, multiplicaciones y divisiones en números inexactos, perderá más y más precisión a medida que se acumulan los errores. Esto hace que los flotadores y los dobles sean inadecuados para tratar con dinero, donde se requiere una precisión perfecta para múltiplos de potencias de base 10 ".
¡Finalmente Java tiene una forma estándar de trabajar con Currency And Money!
JSR 354: API de dinero y divisas
JSR 354 proporciona una API para representar, transportar y realizar cálculos completos con Money and Currency. Puedes descargarlo desde este enlace:
JSR 354: Descarga de API de dinero y divisas
La especificación consta de lo siguiente:
Ejemplos de ejemplo de JSR 354: API de dinero y divisas:
Un ejemplo de crear una MonetaryAmount e imprimirlo en la consola se ve así:
Cuando se usa la API de implementación de referencia, el código necesario es mucho más simple:
La API también admite cálculos con MonetaryAmounts:
CurrencyUnit y MonetaryAmount
MonetaryAmount tiene varios métodos que permiten acceder a la moneda asignada, la cantidad numérica, su precisión y más:
Las cantidades monetarias se pueden redondear utilizando un operador de redondeo:
Al trabajar con colecciones de Montos Monetarios, hay disponibles algunos buenos métodos de utilidad para filtrar, ordenar y agrupar.
Operaciones de Monto Monetario Personalizado
Recursos:
Manejo de dinero y monedas en Java con JSR 354
Analizando la API de dinero y moneda de Java 9 (JSR 354)
Ver también: JSR 354 - Moneda y dinero
fuente
Si su cálculo involucra varios pasos, la aritmética de precisión arbitraria no lo cubrirá al 100%.
La única forma confiable de usar una representación perfecta de resultados (Use un tipo de datos de Fracción personalizado que dividirá las operaciones de división por lotes al último paso) y solo se convertirá a notación decimal en el último paso.
La precisión arbitraria no ayudará porque siempre puede haber números que tengan tantos decimales, o algunos resultados como
0.6666666
... Ninguna representación arbitraria cubrirá el último ejemplo. Entonces tendrá pequeños errores en cada paso.Estos errores se sumarán, y eventualmente ya no será fácil ignorarlos. Esto se llama propagación de error .
fuente
La mayoría de las respuestas han resaltado las razones por las cuales uno no debería usar dobles para los cálculos de dinero y divisas. Y estoy totalmente de acuerdo con ellos.
Sin embargo, no significa que los dobles nunca se puedan usar para ese propósito.
He trabajado en varios proyectos con requisitos gc muy bajos, y tener objetos BigDecimal fue un gran contribuyente a esa sobrecarga.
Es la falta de comprensión sobre la doble representación y la falta de experiencia en el manejo de la precisión y la precisión lo que genera esta sabia sugerencia.
Puede hacer que funcione si puede manejar la precisión y los requisitos de precisión de su proyecto, lo que debe hacerse en función de qué rango de valores dobles se trata.
Puede consultar el método FuzzyCompare de guayaba para tener más idea. La tolerancia del parámetro es la clave. Nos ocupamos de este problema para una aplicación de negociación de valores e hicimos una investigación exhaustiva sobre qué tolerancias usar para diferentes valores numéricos en diferentes rangos.
Además, puede haber situaciones en las que tengas la tentación de usar envoltorios dobles como clave de mapa, siendo el mapa hash la implementación. Es muy arriesgado porque Double.equals y el código hash, por ejemplo, los valores "0.5" y "0.6 - 0.1" causarán un gran desorden.
fuente
Muchas de las respuestas publicadas a esta pregunta discuten IEEE y los estándares que rodean la aritmética de punto flotante.
Al provenir de un entorno no informático (física e ingeniería), tiendo a ver los problemas desde una perspectiva diferente. Para mí, la razón por la que no usaría un doble o flotante en un cálculo matemático es que perdería demasiada información.
Cuales son las alternativas? Hay muchos (¡y muchos más de los que no estoy al tanto!).
BigDecimal en Java es nativo del lenguaje Java. Apfloat es otra biblioteca de precisión arbitraria para Java.
El tipo de datos decimales en C # es la alternativa .NET de Microsoft para 28 cifras significativas.
SciPy (Scientific Python) probablemente también pueda manejar cálculos financieros (no lo he intentado, pero sospecho que sí).
GNU Multiple Precision Library (GMP) y GNU MFPR Library son dos recursos gratuitos y de código abierto para C y C ++.
También hay bibliotecas de precisión numérica para JavaScript (!) Y creo que PHP puede manejar cálculos financieros.
También hay soluciones patentadas (particularmente, creo, para Fortran) y de código abierto, así como para muchos lenguajes de computadora.
No soy un informático por formación. Sin embargo, tiendo a inclinarme hacia BigDecimal en Java o decimal en C #. No he probado las otras soluciones que he enumerado, pero probablemente también sean muy buenas.
Para mí, me gusta BigDecimal debido a los métodos que admite. El decimal de C # es muy bueno, pero no he tenido la oportunidad de trabajar con él tanto como me gustaría. Hago cálculos científicos que me interesan en mi tiempo libre, y BigDecimal parece funcionar muy bien porque puedo establecer la precisión de mis números de coma flotante. ¿La desventaja de BigDecimal? A veces puede ser lento, especialmente si está utilizando el método de división.
Puede que, para mayor velocidad, busque en las bibliotecas gratuitas y propietarias en C, C ++ y Fortran.
fuente
Para agregar respuestas anteriores, también existe la opción de implementar Joda-Money en Java, además de BigDecimal, cuando se trata el problema abordado en la pregunta. El nombre del módulo Java es org.joda.money.
Requiere Java SE 8 o posterior y no tiene dependencias.
Ejemplos de uso de Joda Money:
fuente
Algunos ejemplos ... esto funciona (en realidad no funciona como se esperaba), en casi cualquier lenguaje de programación ... He intentado con Delphi, VBScript, Visual Basic, JavaScript y ahora con Java / Android:
SALIDA:
round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!
fuente
Float es una forma binaria de Decimal con un diseño diferente; Son dos cosas diferentes. Hay pequeños errores entre dos tipos cuando se convierten entre sí. Además, float está diseñado para representar un gran número infinito de valores científicos. Eso significa que está diseñado para perder precisión a un número extremadamente pequeño y extremadamente grande con ese número fijo de bytes. Decimal no puede representar un número infinito de valores, limita solo a ese número de dígitos decimales. Entonces Float y Decimal son para diferentes propósitos.
Hay algunas formas de administrar el error para el valor de la moneda:
Use enteros largos y cuente en centavos en su lugar.
Use doble precisión, mantenga sus dígitos significativos a 15 solo para que el decimal se pueda simular exactamente. Redondear antes de presentar valores; Redondea a menudo cuando haces cálculos.
Use una biblioteca decimal como Java BigDecimal, por lo que no necesita usar el doble para simular el decimal.
PD: es interesante saber que la mayoría de las marcas de calculadoras científicas portátiles funcionan en decimal en lugar de flotante. Por lo tanto, ninguna queja de errores de conversión de flotador.
fuente