¿Por qué no usar Double o Float para representar la moneda?


939

Siempre me han dicho que nunca represente dinero doubleo floattipos, 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.


44
Vea esta pregunta SO: ¿Errores de redondeo?
Jeff Ogata

80
Para ser claros, no deben usarse para nada que requiera precisión, no solo moneda.
Jeff

152
No deben usarse para nada que requiera exactitud . Pero los 53 bits significativos de double (~ 16 dígitos decimales) suelen ser lo suficientemente buenos para cosas que simplemente requieren precisión .
dan04

21
@jeff Su comentario tergiversa por completo para qué punto flotante binario es bueno y para qué no es bueno. Lea la respuesta a continuación, y elimine su comentario engañoso.
Pascal Cuoq

Respuestas:


985

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 doubleo floatprobablemente 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 BigDecimalclase y C # tiene el decimaltipo.


3
@Fran usted conseguirá errores de redondeo y en algunos casos donde se utilizan grandes cantidades de moneda, cálculos de tasas de interés pueden ser groseramente fuera
linuxuser27

55
... la mayoría de las fracciones de base 10, es decir. Por ejemplo, 0.1 no tiene representación binaria exacta de punto flotante. Entonces, 1.0 / 10 * 10puede no ser lo mismo que 1.0.
Chris Jester-Young

66
@ linuxuser27 Creo que Fran estaba tratando de ser gracioso. De todos modos, la respuesta de Zneak es la mejor que he visto, incluso mejor que la versión clásica de Bloch.
Isaac Rabinovitch

55
Por supuesto, si conoce la precisión, siempre puede redondear el resultado y así evitar todo el problema. Esto es mucho más rápido y sencillo que usar BigDecimal. Otra alternativa es usar precisión fija int o long.
Peter Lawrey

2
@zneak sabes que los números racionales son un subconjunto de los números reales, ¿verdad? Los números reales IEEE-754 son números reales. Simplemente resultan ser también racionales.
Tim Seguine

314

De Bloch, J., Effective Java, 2nd ed, Item 48:

Los tipos floaty doubleson particularmente inadecuados para los cálculos monetarios porque es imposible representar 0.1 (o cualquier otra potencia negativa de diez) como a floato doubleexactamente.

Por ejemplo, suponga que tiene $ 1.03 y gasta 42c. ¿Cuánto dinero te queda?

System.out.println(1.03 - .42);

imprime 0.6100000000000001.

La forma correcta de resolver este problema es usar BigDecimal, into long para cálculos monetarios.

Aunque BigDecimaltiene algunas advertencias (consulte la respuesta actualmente aceptada).


66
Estoy un poco confundido por la recomendación de usar int o long para los cálculos monetarios. ¿Cómo representas a 1.03 como int o long? He intentado "long a = 1.04;" y "largo a = 104/100;" en vano.
Peter

49
@Peter, usas long a = 104y cuentas en centavos en lugar de dólares.
zneak

@zneak ¿Qué pasa cuando se necesita aplicar un porcentaje como interés compuesto o similar?
trusktr

3
@trusktr, iría con el tipo decimal de su plataforma. En Java, eso es BigDecimal.
zneak

13
@maaartinus ... ¿y no crees que usar doble para tales cosas es propenso a errores? He visto que el problema del redondeo flotante golpea duro a los sistemas reales . Incluso en la banca. Por favor, no lo recomiendo, o si lo hace, prever que, en una respuesta por separado (para que podamos downvote: P)
EIS

75

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.


2
Relacionado, interesante: En mi consola js de Chrome: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop

16
Esta respuesta es engañosa. 1.40 * 165 = 231. Cualquier número que no sea exactamente 231 es incorrecto en un sentido matemático (y todos los demás sentidos).
Karu

2
@Karu Creo que es por eso que Randy dice que las carrozas son malas ... Mi consola Chrome JS muestra 230.99999999999997 como resultado. Eso está mal, que es el punto señalado en la respuesta.
trusktr

66
@Karu: En mi opinión, la respuesta no es matemáticamente incorrecta. Es solo que hay 2 preguntas, una de ellas no es la pregunta que se hace. La pregunta que responde su compilador es 1.39999999 * 164.99999999 y así sucesivamente, matemáticamente correcto es igual a 230.99999 ... Obviamente, esa no es la pregunta que se hizo en primer lugar ...
markus

2
@CurtisYallop porque el valor doble de cierre a 0.49999999999999999 es 0.5 ¿Por qué Math.round(0.49999999999999994)devuelve 1?
phuclv

53

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

System.out.println(1000000.0f + 1.2f - 1000000.0f);

resultados en

1.1875

16
¡¡¡¡ESTA!!!! ¡Estaba buscando todas las respuestas para encontrar este HECHO RELEVANTE! En los cálculos normales, a nadie le importa si eres de una fracción de centavo, ¡pero aquí con números altos fácilmente se pierden algunos dólares por transacción!
Falco

22
Y ahora imagine que alguien obtiene ingresos diarios de 0.01% en sus 1 millón de dólares, no obtendría nada cada día, y después de un año no ha obtenido 1000 dólares, ESTO IMPORTARÁ
Falco

66
El problema no es la precisión, pero ese flotador no te dice que se vuelve inexacto. Un número entero solo puede contener hasta 10 dígitos, un flotador puede contener hasta 6 sin volverse inexacto (cuando lo corta en consecuencia). Sí permite esto mientras que un número entero se desborda y un lenguaje como java lo advertirá o no lo permitirá. Cuando usa un doble, puede ir hasta 16 dígitos, que es suficiente para muchos casos de uso.
sigi

39

Flotadores y dobles son aproximados. Si crea un BigDecimal y pasa un flotador al constructor, verá lo que realmente es el flotador:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

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.


55
float, doubley BigDecimalrepresentan valores exactos . La conversión de código a objeto es inexacta, así como otras operaciones. Los tipos en sí mismos no son inexactos.
chux - Restablecer Monica

1
@chux: releyendo esto, creo que tienes un punto de que mi redacción podría mejorarse. Voy a editar esto y reformularlo.
Nathan Hughes

28

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.


3
Esta es realmente una respuesta bastante decente. En la mayoría de los casos, está perfectamente bien usarlos.
Vahid Amiri

2
Cabe señalar que la mayoría de los bancos de inversión utilizan el doble que la mayoría de los programas de C ++. Algunos usan mucho tiempo, pero por lo tanto tiene su propio problema de seguimiento de escala.
Peter Lawrey

20

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.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

15

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.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

Salida:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

3
Probemos algo más que la suma / resta trivial y la multiplicación entera, si el código calculara la tasa mensual de un préstamo del 7%, ambos tipos no podrían proporcionar un valor exacto y necesitarían redondear al 0.01 más cercano. El redondeo a la unidad monetaria más baja es parte de los cálculos de dinero. El uso de tipos decimales evita esa necesidad con la suma / resta, pero no mucho más.
chux

@ chux-ReinstateMonica: Si se supone que el interés se capitaliza mensualmente, calcule el interés cada mes sumando el saldo diario, multiplíquelo por 7 (la tasa de interés) y divida, redondeando al centavo más cercano, por la cantidad de días en el año. Sin redondeo en ninguna parte, excepto una vez al mes en el último paso.
supercat

@supercat Mi comentario enfatiza el uso de un FP binario de la unidad monetaria más pequeña o un FP decimal, ambos tienen problemas de redondeo similares, como en su comentario con "y dividir, redondeando al centavo más cercano". El uso de un FP base 2 o base 10 no proporciona una ventaja de ninguna manera en su escenario.
chux - Restablece a Mónica

@ chux-ReinstateMonica: en el escenario anterior, si la matemática resulta que el interés debe ser exactamente igual a un cierto número de medios centavos, un programa financiero correcto debe redondearse de manera precisa. Si los cálculos de punto flotante arrojan un valor de interés de, por ejemplo, $ 1.23499941, pero el valor matemáticamente preciso antes del redondeo debería haber sido de $ 1.235 y el redondeo se especifica como "par más cercano", el uso de tales cálculos de punto flotante no causará el resultado estar fuera por $ 0.000059, sino por un total de $ 0.01, que para fines contables es simplemente incorrecto.
supercat

@supercat El uso de doubleFP 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.
chux - Restablece a Mónica

11

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:

  1. Una API para manejar, por ejemplo, montos monetarios y monedas
  2. API para soportar implementaciones intercambiables
  3. Fábricas para crear instancias de las clases de implementación
  4. Funcionalidad para cálculos, conversión y formateo de importes monetarios.
  5. API de Java para trabajar con dinero y monedas, que se planea incluir en Java 9.
  6. Todas las clases e interfaces de especificación se encuentran en el paquete javax.money. *.

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í:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Cuando se usa la API de implementación de referencia, el código necesario es mucho más simple:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

La API también admite cálculos con MonetaryAmounts:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit y MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount tiene varios métodos que permiten acceder a la moneda asignada, la cantidad numérica, su precisión y más:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

Las cantidades monetarias se pueden redondear utilizando un operador de redondeo:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

Al trabajar con colecciones de Montos Monetarios, hay disponibles algunos buenos métodos de utilidad para filtrar, ordenar y agrupar.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Operaciones de Monto Monetario Personalizado

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

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


5

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 .


4

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.


2

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.


1
Con respecto a SciPy / Numpy, la precisión fija (es decir, decimal.Decimal de Python) no es compatible ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Algunas funciones no funcionarán correctamente con Decimal (isnan, por ejemplo). Pandas se basa en Numpy y se inició en AQR, uno de los principales fondos de cobertura cuantitativos. Entonces tiene su respuesta con respecto a los cálculos financieros (no a la contabilidad de comestibles).
Comte

2

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.

Para ser más precisos, existe una dependencia en tiempo de compilación pero no es necesaria.

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

Ejemplos de uso de Joda Money:

  // create a monetary value
  Money money = Money.parse("USD 23.87");

  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));

  // subtracts an amount in dollars
  money = money.minusMajor(2);

  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);

  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);

  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);

  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

Documentación: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Ejemplos de implementación: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money


0

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:

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) {
        total += 0.1;  // adds 10 cents
    }

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) {
        total -= 0.1;  // removes 10 cents
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

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!!!


3
El problema no es que ocurra un error de redondeo, sino que no lo solucione. Redondea el resultado a dos decimales (si quieres centavos) y listo.
maaartinus

0

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:

  1. Use enteros largos y cuente en centavos en su lugar.

  2. 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.

  3. 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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.