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


Siempre me han dicho Nunca para representar el dinero con double o float tipos, y esta vez te planteo la pregunta: ¿por qué?

Estoy seguro de que hay una muy buena razón, simplemente no sé de qué se trata.


763
2017-09-16 19:23


origen


Respuestas:


Porque las carrozas y los dobles no pueden representar con precisión los múltiplos de la base 10 que usamos por 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). Números de coma flotante IEEE-754 son diferentes, pero una manera muy simple de pensar sobre ellos es multiplicar por una potencia de dos en su lugar. Por ejemplo, podrías mirar 164 * 2-4 (un 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 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 ningún entero finito que pueda multiplicar por una potencia de 10 para obtener 1/3. Podría establecerse en una larga secuencia de 3 y un pequeño exponente, 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 se valora dentro de un orden de magnitud del dólar estadounidense, generalmente todo lo que necesita es poder almacenar múltiplos de 10.-2, entonces realmente no importa que 1/3 no pueda ser representado.

El problema con los flotadores y los dobles es que amplia mayoría de números similares a 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 de dinero porque son centavos enteros) que pueden se 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 desactivados por una pequeña cantidad. Como una analogía con el ejemplo 0.333333, si toma el valor de coma flotante para 0.1 y lo multiplica por 10, no obtendrá 1.

Representando el dinero como una double o float probablemente se vea bien al principio, ya que el software redondea los pequeños errores, pero a medida que realiza más adiciones, restas, multiplicaciones y divisiones en números inexactos, los errores se acumularán y terminará con valores que son visiblemente no precisos. Esto hace que los flotadores y los dobles sean inadecuados para tratar con dinero, donde se requiere precisión perfecta para múltiplos de potencias de base 10.

Una solución que funciona en casi cualquier idioma es usar enteros en su lugar, y contar centavos. Por ejemplo, 1025 sería de $ 10.25. Varios idiomas también tienen tipos incorporados para manejar el dinero. Entre otros, Java tiene el BigDecimal clase, y C # tiene el decimal tipo.


788
2017-09-16 19:26



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

los float y double tipos son   particularmente inadecuado para monetario   cálculos porque es imposible   representar 0.1 (o cualquier otro   poder negativo de diez) como una float o    double exactamente.

Por ejemplo, supongamos que tiene $ 1.03   y gastas 42c. Cuanto dinero hacer   ¿te has ido?

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

imprime 0.6100000000000001.

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


270
2017-09-16 19:52



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 seres humanos que usan la base 10 para los cálculos en lugar de la base 2. Por ejemplo, usar dobles para los cálculos financieros no produce respuestas que son "incorrectas" en un sentido matemático, pero puede producir respuestas que son no lo que se espera en un sentido financiero.

Incluso si redondea sus resultados en el último minuto antes de la salida, de todos modos puede obtener un resultado ocasionalmente 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 truncas el número, obtienes 230 en lugar de 231. Puedes razonar que el redondeo en lugar de truncar han dado el resultado deseado de 231. Eso es cierto, pero el redondeo siempre implica el truncamiento. Independientemente de la técnica de redondeo que utilice, todavía existen condiciones de contorno como esta que redondearán cuando espere que redondee. Son lo suficientemente raros que a menudo no se encontrarán mediante pruebas u observaciones casuales. Puede que tenga que escribir un código para buscar ejemplos que ilustren resultados que no se comporten como se espera.

Supongamos que quiere redondear algo al centavo más cercano. Entonces, toma su resultado final, multiplique por 100, agregue 0.5, trunque, luego divida el resultado entre 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 redondee el número al centavo más cercano. Pero sus cálculos de la base 10 pueden haber indicado que la respuesta debería ser exactamente 3.465, que claramente debería redondearse a 3.47, no a 3.46. Este tipo de cosas suceden ocasionalmente en la vida real cuando usa dobles para cálculos financieros. Es raro, por lo que a menudo pasa desapercibido como un problema, pero sucede.

Si usa la base 10 para sus cálculos internos en lugar de los dobles, las respuestas siempre son exactamente las esperadas por los humanos, suponiendo que no haya otros errores en su código.


63
2017-09-12 15:11



Estoy preocupado por algunas de estas respuestas. Creo que los dobles y las carrozas tienen un lugar en los cálculos financieros. Ciertamente, al sumar y restar montos monetarios no fraccionarios, no habrá pérdida de precisión cuando se utilicen clases de enteros o BigDecimal. Pero cuando se realizan operaciones más complejas, a menudo se obtienen resultados que salen 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 redondearse hacia arriba y redondearse hacia abajo, y ese último centavo realmente importa, probablemente debería estar diciéndole al espectador que la respuesta está casi en el medio, al mostrar más decimales.

El problema con los dobles, y más con los flotadores, es cuando se usan para combinar números grandes y pequeños. En Java,

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

resultados en

1.1875

40
2018-04-03 14:54



Flotadores y dobles son aproximados. Si creas un BigDecimal y pasas un float al constructor ves lo que el flotador realmente iguala:

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

probablemente esta no es la forma en que 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 repetitivas, por lo que terminas con errores de aproximación. Como a los contadores les gusta que las cosas salgan exactamente a la ligera, y los clientes se molestarán si pagan su factura y después de que se procesa el pago, adeudan .01 y les cobran una tarifa o no pueden cerrar su cuenta, es mejor usarla tipos exactos como decimal (en C #) o java.math.BigDecimal en Java.

No es que el error no sea controlable si redondeas: vea este artículo de Peter Lawrey. En primer lugar, es más fácil no tener que redondear. La mayoría de las aplicaciones que manejan dinero no requieren mucha matemática, las operaciones consisten en agregar cosas o asignar montos a diferentes categorías. Introducir el punto flotante y el redondeo solo complica las cosas.


35
2017-09-16 19:29



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. flotante y doble 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 punto flotante puede variar de JVM a JVM. Mire el siguiente ejemplo de BigDecimal y doble primitiva 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 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

15
2017-08-11 20:18



Si bien es cierto que el tipo de punto flotante solo puede representar datos decimales aproximados, también es cierto que si se redondean los números a la precisión necesaria antes de presentarlos, se obtiene el resultado correcto. Generalmente.

Usualmente porque el tipo doble tiene una precisión de menos de 16 figuras. Si necesita una mayor precisión, no es un tipo adecuado. También se pueden acumular aproximaciones.

Debe decirse que incluso si usa aritmética de punto fijo, debe redondear los números, de no ser por el hecho de que BigInteger y BigDecimal generan errores si obtiene números decimales periódicos. Entonces hay una aproximación también aquí.

Por ejemplo, COBOL, utilizado históricamente para cálculos financieros, tiene una precisión máxima de 18 cifras. Entonces, a menudo hay un redondeo implícito.

Para concluir, en mi opinión, el doble no es adecuado principalmente para su precisión de 16 dígitos, que puede ser insuficiente, no porque sea aproximado.

Considere la siguiente salida del siguiente programa. Muestra que después del redondeo doble da el mismo resultado que BigDecimal hasta 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;
    }
}

14
2017-10-26 19:03



Me arriesgaré a ser votado negativamente, pero creo que la inadecuación de los números flotantes para los cálculos de monedas está sobrevalorada. Siempre que se asegure de hacer el redondeo de centavos correctamente y tener suficientes dígitos significativos para trabajar con el fin de contrarrestar la falta de correspondencia representación decimal binaria explicada por zneak, no habrá ningún problema.

Las personas que calculan con moneda en Excel siempre han utilizado flotantes de doble precisión (no hay ningún tipo de moneda en Excel) y todavía no he visto a nadie quejarse sobre errores de redondeo.

Por supuesto, debes permanecer dentro de lo razonable; p.ej. una tienda web simple probablemente nunca experimente ningún problema con los flotadores de doble precisión, pero si lo hace, p. contabilidad o cualquier otra cosa que requiera agregar una cantidad grande (no restringida) de números, no querrá tocar números flotantes con un polo de diez pies.


10
2018-01-20 11:56



Como se dijo antes "Representar el dinero como un doble o flotar probablemente se vea bien al principio a medida que el software redondea los pequeños errores, pero a medida que realiza más adiciones, restas, multiplicaciones y divisiones en números inexactos, perderá más y más precisión a medida que los errores se suman. Esto hace que las carrozas y los dobles sean inadecuados para manejar el dinero, donde se requiere una precisión perfecta para los múltiplos de las potencias de la 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 integrales con dinero y moneda. Puedes descargarlo desde este enlace:

JSR 354: Descargar la API de dinero y divisas

La especificación consiste en las siguientes cosas:

  1. Una API para manejar e. gramo. montos monetarios y monedas
  2. API para admitir 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 divisas, que se planea incluir en Java 9.
  6. Todas las clases de especificación e interfaces se encuentran en el paquete javax.money. *.

Ejemplos de muestra de JSR 354: Money and Currency API:

Un ejemplo de cómo crear MonetaryAmount e imprimirlo en la consola es similar a este ::

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 utiliza 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;

MonetaryAmounts se puede 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 MonetaryAmounts, hay algunos buenos métodos de utilidad para filtrar, clasificar y agrupar disponibles.

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 MonetaryAmount personalizadas

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

Examinando la API de dinero y divisas de Java 9 (JSR 354)

Ver también: JSR 354 - Moneda y dinero


9
2017-08-07 07:08



Si su cálculo implica varios pasos, la aritmética de precisión arbitraria no lo cubrirá al 100%.

La única forma confiable de utilizar una representación perfecta de los resultados (use un tipo de datos fraccionado personalizado que agrupe las operaciones de división en el último paso) y solo convierta a una notación decimal en el último paso.

La precisión arbitraria no ayudará porque siempre puede haber números que tengan tantos lugares decimales, o algunos resultados como 0.6666666 ... Ninguna representación arbitraria cubrirá el último ejemplo. Entonces tendrás pequeños errores en cada paso.

Estos errores se acumularán, puede que a la larga no sea fácil ignorarlos. Se llama Propagación de error.


2
2018-01-05 14:56