Por que não usar Double ou Float para representar moeda?

939

Sempre me disseram para nunca representar dinheiro com doubleou com floattipos, e desta vez faço a pergunta para você: por quê?

Tenho certeza de que há uma boa razão, simplesmente não sei o que é.

Fran Fitzpatrick
fonte
4
Veja esta pergunta SO: Erros de arredondamento?
Jeff Ogata
80
Só para esclarecer, eles não devem ser usados ​​para qualquer coisa que exija precisão - não apenas moeda.
Jeff
152
Eles não devem ser usados ​​para nada que exija exatidão . Mas os 53 bits significativos do dobro (~ 16 dígitos decimais) geralmente são bons o suficiente para coisas que meramente exigem precisão .
Dan04 17/09/10
21
@jeff Seu comentário deturpa completamente para que serve o ponto flutuante binário e para que não serve. Leia a resposta do zneak abaixo e exclua seu comentário enganoso.
Pascal Cuoq

Respostas:

985

Porque floats e doubles não podem representar com precisão os múltiplos da base 10 que usamos para dinheiro. Esse problema não é apenas para Java, é para qualquer linguagem de programação que use tipos de ponto flutuante base 2.

Na base 10, você pode escrever 10,25 como 1025 * 10 -2 (um número inteiro vezes a potência de 10). Os números de ponto flutuante IEEE-754 são diferentes, mas uma maneira muito simples de pensar sobre eles é multiplicar por uma potência de dois. Por exemplo, você pode observar 164 * 2 -4 (um número inteiro vezes a potência de dois), que também é igual a 10,25. Não é assim que os números são representados na memória, mas as implicações matemáticas são as mesmas.

Mesmo na base 10, essa notação não pode representar com precisão a maioria das frações simples. Por exemplo, você não pode representar 1/3: a representação decimal está se repetindo (0,3333 ...); portanto, não há um número inteiro finito que possa ser multiplicado por uma potência de 10 para obter 1/3. Você pode optar por uma longa sequência de 3 e um pequeno expoente, como 333333333 * 10-10 , mas não é preciso: se você multiplicar por 3, não receberá 1.

No entanto, com o objetivo de contar dinheiro, pelo menos para países cujo dinheiro é avaliado dentro de uma ordem de magnitude do dólar americano, geralmente tudo o que você precisa é ser capaz de armazenar múltiplos de 10 -2 , então isso realmente não importa que 1/3 não pode ser representado.

O problema com números flutuantes e duplos é que a grande maioria dos números semelhantes a dinheiro não tem uma representação exata como um número inteiro vezes uma potência de 2. De fato, os únicos múltiplos de 0,01 entre 0 e 1 (que são significativos ao lidar com dinheiro porque eles são centavos inteiros) que podem ser representados exatamente como um número de ponto flutuante binário IEEE-754 são 0, 0,25, 0,5, 0,75 e 1. Todos os outros são desativados por uma pequena quantia. Como uma analogia ao exemplo 0,333333, se você pegar o valor do ponto flutuante para 0,1 e multiplicá-lo por 10, não receberá 1.

Representar o dinheiro como doubleou floatprovavelmente parecerá bom no começo, à medida que o software encerra os pequenos erros, mas à medida que você executa mais adições, subtrações, multiplicações e divisões em números inexatos, os erros serão compostos e você terá valores visivelmente visíveis. não é preciso. Isso torna flutuações e duplas inadequadas para lidar com dinheiro, onde é necessária uma precisão perfeita para múltiplos de poderes da base 10.

Uma solução que funciona em praticamente qualquer idioma é usar números inteiros e contar centavos. Por exemplo, 1025 seria $ 10,25. Vários idiomas também possuem tipos internos para lidar com dinheiro. Entre outros, Java tem a BigDecimalclasse e C # tem o decimaltipo

zneak
fonte
3
@Fran você receberá erros de arredondamento e em alguns casos em que grandes quantidades de moeda estão a ser utilizados, os cálculos de taxas de juros pode ser grosseiramente fora
linuxuser27
5
... a maioria das 10 frações básicas, isto é. Por exemplo, 0.1 não tem representação exata de ponto flutuante binário. Portanto, 1.0 / 10 * 10pode não ser o mesmo que 1.0.
Chris Jester-Young
6
@ linuxuser27 Acho que Fran estava tentando ser engraçado. De qualquer forma, a resposta do zneak é a melhor que eu já vi, melhor do que a versão clássica de Bloch.
Isaac Rabinovitch
5
Obviamente, se você conhece a precisão, sempre pode arredondar o resultado e evitar todo o problema. Isso é muito mais rápido e simples do que usar o BigDecimal. Outra alternativa é usar precisão fixa int ou long.
27413 Peter Lawrey
2
@ zneak você sabe que os números racionais são um subconjunto dos números reais, certo? Os números reais IEEE-754 são números reais. Eles também são racionais.
Tim Seguine
314

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

Os tipos floate doublesão particularmente inadequados para cálculos monetários porque é impossível representar 0,1 (ou qualquer outro poder negativo de dez) como floatou doubleexatamente.

Por exemplo, suponha que você tenha US $ 1,03 e gaste 42c. Quanto você tem?

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

imprime 0.6100000000000001.

O caminho certo para resolver este problema é usar BigDecimal, intou long para cálculos monetários.

Embora BigDecimaltenha algumas advertências (consulte a resposta atualmente aceita).

dogbane
fonte
6
Estou um pouco confuso com a recomendação de usar int ou long para cálculos monetários. Como você representa 1,03 como int ou long? Eu tentei "long a = 1.04;" e "longo a = 104/100;" para nenhum proveito.
Peter Peter
49
@ Peter, você usa long a = 104e conta em centavos em vez de dólares.
zneak
@zneak Que tal quando uma porcentagem precisa ser aplicada como juros compostos ou similares?
trusktr
3
@trusktr, eu usaria o tipo decimal da sua plataforma. Em Java, é isso BigDecimal.
Zneak 6/03/16
13
@maaartinus ... e você não acha que usar o dobro para essas coisas é propenso a erros? Eu já vi o problema de arredondamento de flutuador atingir sistemas reais com força . Mesmo no setor bancário. Não o recomende ou, se o fizer, forneça isso como uma resposta separada (para que possamos reduzir o voto: P)
eis
75

Isso não é uma questão de precisão, nem é uma questão de precisão. É uma questão de atender às expectativas de humanos que usam a base 10 para cálculos em vez da base 2. Por exemplo, o uso de duplicatas para cálculos financeiros não produz respostas "erradas" no sentido matemático, mas pode produzir respostas que são não o que é esperado em um sentido financeiro.

Mesmo se você arredondar seus resultados no último minuto antes da saída, ainda poderá obter resultados ocasionalmente usando duplas que não correspondam às expectativas.

Usando uma calculadora ou calculando os resultados manualmente, 1,40 * 165 = 231 exatamente. No entanto, usando internamente dobros, no meu ambiente de compilador / sistema operacional, ele é armazenado como um número binário próximo a 230.99999 ... portanto, se você truncar o número, obterá 230 em vez de 231. Você pode considerar que o arredondamento em vez de truncamento seria deram o resultado desejado de 231. Isso é verdade, mas o arredondamento sempre envolve truncamento. Qualquer que seja a técnica de arredondamento que você use, ainda existem condições de contorno como essa que serão arredondadas quando você espera que ela seja arredondada. Eles são raros o suficiente para não serem encontrados por meio de testes ou observações casuais. Pode ser necessário escrever algum código para procurar exemplos que ilustrem resultados que não se comportam conforme o esperado.

Suponha que você queira arredondar algo para o centavo mais próximo. Então você pega o resultado final, multiplica por 100, adiciona 0,5, trunca e depois divide o resultado por 100 para voltar aos centavos. Se o número interno armazenado for 3,46499999 .... em vez de 3,465, você receberá 3,46 em vez de 3,47 quando arredondar o número para o centavo mais próximo. Mas seus cálculos de base 10 podem ter indicado que a resposta deve ser exatamente 3.465, o que claramente deve arredondar para 3,47, e não 3,46. Ocasionalmente, esses tipos de coisas acontecem na vida real quando você usa duplas para cálculos financeiros. É raro, portanto, muitas vezes passa despercebido como um problema, mas acontece.

Se você usar a base 10 para seus cálculos internos, em vez de duplicar, as respostas serão sempre exatamente o que é esperado pelos humanos, assumindo que não há outros erros no seu código.

Randy D Oxentenko
fonte
2
Relacionado, interessante: Na minha cromo js console: Math.round (,4999999999999999): 0 Math.round (,49999999999999999): 1
Curtis Yallop
16
Esta resposta é enganosa. 1,40 * 165 = 231. Qualquer número que não seja exatamente 231 está errado no sentido matemático (e todos os outros sentidos).
quer
2
@Karu Acho que é por isso que Randy diz que os carros alegóricos são ruins ... Meu console do Chrome JS mostra 230.99999999999997 como resultado. Isso está errado, e é esse o argumento da resposta.
trusktr
6
@ Karu: Imho a resposta não está matematicamente errada. Apenas duas perguntas estão sendo respondidas e essa não é a pergunta que está sendo feita. A pergunta suas respostas compilador é 1.39999999 * 164.99999999 e assim por diante o que equivale matematicamente corretos 230.99999 .... Obviamente tha não é a pergunta que foi feita no primeiro lugar ....
Markus
2
@CurtisYallop porque o valor duplo fechado para 0,49999999999999999 é 0,5 Por que Math.round(0.49999999999999994)retornar 1?
Phuclv
53

Estou preocupado com algumas dessas respostas. Acho que duplas e carros alegóricos têm um lugar nos cálculos financeiros. Certamente, ao adicionar e subtrair valores monetários não fracionários, não haverá perda de precisão ao usar classes inteiras ou BigDecimal. Mas, ao executar operações mais complexas, você geralmente obtém resultados com várias ou várias casas decimais, independentemente de como armazena os números. A questão é como você apresenta o resultado.

Se o resultado estiver na fronteira entre ser arredondado para cima e para baixo, e esse último centavo realmente importa, você provavelmente deve estar dizendo ao espectador que a resposta está quase no meio - exibindo mais casas decimais.

O problema com duplos e, mais ainda, com carros alegóricos, é quando eles são usados ​​para combinar grandes números e pequenos números. Em java,

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

resulta em

1.1875
Rob Scala
fonte
16
ISTO!!!! Eu estava procurando todas as respostas para encontrar esse FATO RELEVANTE !!! Em cálculos normais, ninguém se importa se você tem uma fração de centavo, mas aqui com números altos facilmente alguns dólares são perdidos por transação!
Falco
22
E agora imagine alguém tirando receita diária de 0,01% em seus 1 milhão de dólares - ele iria receber nada cada dia - e depois de um ano ele não tenha chegado de 1000 dólares, isso vai MATTER
Falco
6
O problema não é a precisão, mas esse flutuador não informa que ele se torna impreciso. Um número inteiro pode conter apenas 10 dígitos e um número flutuante pode conter até 6 sem se tornar impreciso (quando você o corta adequadamente). Ele permite isso enquanto um número inteiro recebe um estouro e um idioma como java o avisa ou não. Quando você usa um duplo, pode subir até 16 dígitos, o que é suficiente para muitos casos de uso.
Signi
39

Carros alegóricos e duplos são aproximados. Se você criar um BigDecimal e passar um float para o construtor, verá o que o float realmente é igual:

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

provavelmente não é assim que você deseja representar US $ 1,01.

O problema é que a especificação IEEE não tem uma maneira exata de representar todas as frações, algumas delas acabam como frações repetidas e você acaba com erros de aproximação. Como os contadores gostam que as coisas saiam exatamente ao centavo, e os clientes ficam irritados se pagarem a conta e, após o processamento do pagamento, eles devem .01 e recebem uma taxa ou não conseguem fechar a conta, é melhor usar tipos exatos como decimal (em C #) ou java.math.BigDecimal em Java.

Não é que o erro não seja controlável se você arredondar: veja este artigo de Peter Lawrey . É mais fácil não ter que arredondar em primeiro lugar. A maioria dos aplicativos que lidam com dinheiro não exige muita matemática, as operações consistem em adicionar itens ou alocar valores a diferentes baldes. A introdução de ponto flutuante e arredondamento apenas complica as coisas.

Nathan Hughes
fonte
5
float, doublee BigDecimalsão valores exatos . A conversão de código em objeto é inexata e outras operações. Os tipos em si não são inexatos.
chux - Restabelece Monica
1
@chux: relendo isso, acho que você tem um ponto em que minha redação pode ser melhorada. Vou editar isso e reformular.
Nathan Hughes
28

Correrei o risco de ter o voto negativo, mas acho que a inadequação de números de ponto flutuante para cálculos de moeda é superestimada. Desde que você faça o arredondamento de centavos corretamente e tenha dígitos significativos suficientes para trabalhar, a fim de combater a incompatibilidade de representação decimal binária explicada pelo zneak, não haverá problema.

As pessoas que calculam com moeda no Excel sempre usaram flutuadores de precisão dupla (não há tipo de moeda no Excel) e ainda tenho que ver alguém reclamando de erros de arredondamento.

Claro, você tem que ficar dentro da razão; por exemplo, uma loja virtual simples provavelmente nunca enfrentaria nenhum problema com flutuadores de precisão dupla, mas se você fizer, por exemplo, contabilidade ou qualquer outra coisa que exija a adição de uma quantidade grande (irrestrita) de números, você não desejaria tocar em números de ponto flutuante com um metro e meio. pólo.

escitalopram
fonte
3
Esta é realmente uma resposta bastante decente. Na maioria dos casos, é perfeitamente bom usá-los.
Vahid Amiri
2
Note-se que a maioria dos bancos de investimento usa o dobro da maioria dos programas C ++. Alguns usam muito tempo, mas, portanto, têm seu próprio problema de rastreamento de escala.
Peter Lawrey
20

Embora seja verdade que o tipo de ponto flutuante possa representar apenas dados aproximadamente decimais, também é verdade que, se arredondarmos os números para a precisão necessária antes de apresentá-los, obteremos o resultado correto. Usualmente.

Geralmente porque o tipo duplo tem uma precisão menor que 16 algarismos. Se você precisar de melhor precisão, não é um tipo adequado. Também aproximações podem se acumular.

Deve-se dizer que, mesmo se você usar aritmética de ponto fixo, ainda precisará arredondar números, não foi o fato de BigInteger e BigDecimal cometerem erros se você obtiver números decimais periódicos. Portanto, há uma aproximação também aqui.

Por exemplo, o COBOL, usado historicamente para cálculos financeiros, tem uma precisão máxima de 18 dígitos. Portanto, geralmente há um arredondamento implícito.

Concluindo, na minha opinião, o duplo é inadequado principalmente pela precisão de 16 dígitos, que pode ser insuficiente, não porque é aproximado.

Considere a seguinte saída do programa subseqüente. Isso mostra que, após o arredondamento duplo, obtém o mesmo resultado que BigDecimal com precisão 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;
    }
}
user1593165
fonte
15

O resultado do número de ponto flutuante não é exato, o que os torna inadequados para qualquer cálculo financeiro que exija resultado exato e não aproximação. float e double são projetados para cálculos científicos e de engenharia e muitas vezes não produzem resultado exato, também o resultado do cálculo de ponto flutuante pode variar de JVM para JVM. Veja o exemplo abaixo de BigDecimal e primitivo duplo, que é usado para representar o valor monetário, é claro que o cálculo de ponto flutuante pode não ser exato e deve-se usar o BigDecimal para cálculos financeiros.

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

Resultado:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
Dheeraj Arora
fonte
3
Vamos tentar algo diferente de adição / subtração trivial e mutação inteira. Se o código calculasse a taxa mensal de um empréstimo de 7%, os dois tipos precisariam falhar em fornecer um valor exato e arredondar para o 0,01 mais próximo. Arredondar para a unidade monetária mais baixa faz parte dos cálculos monetários. O uso de tipos decimais evita essa necessidade com adição / subtração - mas não muito mais.
chux - Restabelece Monica
@ chux-ReinstateMonica: se os juros devem ser compostos mensalmente, calcule os juros todos os meses adicionando o saldo diário, multiplique por 7 (a taxa de juros) e divida, arredondando para o centavo mais próximo, pelo número de dias em o ano. Nenhum arredondamento em qualquer lugar, exceto uma vez por mês, na última etapa.
supercat
@supercat Meu comentário enfatiza que o uso de um FP binário da menor unidade monetária ou de um FP decimal gera problemas de arredondamento semelhantes - como no seu comentário com "e divide, arredondando para o centavo mais próximo". Usar um FP base 2 ou base 10 não oferece uma vantagem de qualquer maneira no seu cenário.
chux - Restabelece Monica
@ chux-ReinstateMonica: No cenário acima, se a matemática calcular que o interesse deve ser precisamente igual a algum número de meio centavo, um programa financeiro correto deve ser arredondado da maneira especificada com precisão. Se os cálculos de ponto flutuante gerarem um valor de juros de, por exemplo, US $ 1,23499941, mas o valor matematicamente preciso antes do arredondamento for de US $ 1,235 e o arredondamento for especificado como "o par mais próximo", o uso desses cálculos de ponto flutuante não causará o resultado. por US $ 0,000059, mas sim por US $ 0,01, o que, para fins contábeis, é simplesmente errado.
supercat
@supercat Usar doubleFP binário para o centavo não teria problemas em calcular para 0,5 centavo, assim como FP decimal. Se os cálculos de ponto flutuante gerarem um valor de juros de, por exemplo, 123.499941 ¢, por meio de FP binário ou FP decimal, o problema do arredondamento duplo é o mesmo - sem nenhuma vantagem. Sua premissa parece assumir o valor matematicamente preciso e o FP decimal é o mesmo - algo que mesmo o FP decimal não garante. 0.5 / 7.0 * 7.0 é um problema para FP binário e deicmal. IAC, a maioria será discutida, pois espero que a próxima versão do C forneça FP decimal.
chux - Restabelece Monica
11

Como dito anteriormente "Representar dinheiro como um double ou float provavelmente parecerá bom a princípio, pois o software encerra pequenos erros, mas à medida que você executa mais adições, subtrações, multiplicações e divisões em números inexatos, você perde mais e mais precisão à medida que os erros aumentam. Isso torna flutuações e duplas inadequadas para lidar com dinheiro, onde é necessária precisão perfeita para múltiplos de poderes da base 10 ".

Finalmente, o Java tem uma maneira padrão de trabalhar com moeda e dinheiro!

JSR 354: API de Dinheiro e Moeda

O JSR 354 fornece uma API para representar, transportar e executar cálculos abrangentes com dinheiro e moeda. Você pode baixá-lo neste link:

JSR 354: Download da API de Dinheiro e Moeda

A especificação consiste no seguinte:

  1. Uma API para lidar com, por exemplo, valores monetários e moedas
  2. APIs para suportar implementações intercambiáveis
  3. Fábricas para criar instâncias das classes de implementação
  4. Funcionalidade para cálculos, conversão e formatação de valores monetários
  5. API Java para trabalhar com Money and Currencies, planejada para ser incluída no Java 9.
  6. Todas as classes e interfaces de especificação estão localizadas no pacote javax.money. *.

Exemplos de exemplo da JSR 354: API Money e Currency:

Um exemplo de criação de um MonetaryAmount e impressão no console é semelhante 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));

Ao usar a API de implementação de referência, o código necessário é muito mais simples:

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

A API também suporta cálculos com MonetaryAmounts:

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

CurrencyUnit e MonetaryAmount

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

MonetaryAmount possui vários métodos que permitem acessar a moeda atribuída, o valor numérico, sua precisão e muito mais:

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;

As MonetaryAmounts podem ser arredondadas usando um operador de arredondamento:

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

Ao trabalhar com coleções de MonetaryAmounts, estão disponíveis alguns métodos úteis de filtragem, classificação e agrupamento.

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"));

Operações personalizadas MonetaryAmount

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

Manipulando Dinheiro e Moedas em Java com JSR 354

Analisando a API Java 9 Money and Currency (JSR 354)

Veja também: JSR 354 - Moeda e Dinheiro

Affy
fonte
5

Se o seu cálculo envolver várias etapas, a aritmética de precisão arbitrária não cobrirá 100%.

A única maneira confiável de usar uma representação perfeita dos resultados (use um tipo de dados de Fração personalizado que irá agrupar as operações de divisão em lote até a última etapa) e apenas converter em notação decimal na última etapa.

A precisão arbitrária não ajuda, porque sempre pode haver números com tantas casas decimais ou alguns resultados como 0.6666666... Nenhuma representação arbitrária abordará o último exemplo. Então você terá pequenos erros em cada etapa.

Esses erros serão adicionados, eventualmente, não será mais fácil ignorar. Isso é chamado de propagação de erros .

Kemal Dağ
fonte
4

A maioria das respostas destacou as razões pelas quais não se deve usar dobras para cálculos de dinheiro e moeda. E eu concordo totalmente com eles.

Isso não significa que os duplos nunca possam ser usados ​​para esse fim.

Eu trabalhei em vários projetos com requisitos muito baixos de GC, e ter objetos BigDecimal foi um grande contribuinte para essa sobrecarga.

É a falta de entendimento sobre a dupla representação e a falta de experiência em lidar com a exatidão e precisão que traz essa sugestão sábia.

Você pode fazê-lo funcionar se for capaz de lidar com os requisitos de precisão e exatidão do seu projeto, o que deve ser feito com base em qual faixa de valores duplos é uma delas.

Você pode consultar o método FuzzyCompare da goiaba para ter uma idéia melhor. A tolerância do parâmetro é a chave. Lidamos com esse problema para um aplicativo de negociação de valores mobiliários e fizemos uma pesquisa exaustiva sobre quais tolerâncias usar para diferentes valores numéricos em diferentes faixas.

Além disso, pode haver situações em que você é tentado a usar wrappers Double como uma chave de mapa, com o mapa de hash sendo a implementação. É muito arriscado porque Double.equals e código de hash, por exemplo, valores "0.5" e "0.6 - 0.1" causarão uma grande bagunça.

Dev Amitabh
fonte
2

Muitas das respostas postadas nesta pergunta discutem o IEEE e os padrões em torno da aritmética de ponto flutuante.

Vindo de uma formação que não seja de informática (física e engenharia), tenho a tendência de analisar os problemas de uma perspectiva diferente. Para mim, a razão pela qual eu não usaria um double ou float em um cálculo matemático é que perderia muita informação.

Quais são as alternativas? Existem muitos (e muitos mais dos quais não estou ciente!).

BigDecimal em Java é nativo da linguagem Java. O Apfloat é outra biblioteca de precisão arbitrária para Java.

O tipo de dados decimal em C # é a alternativa .NET da Microsoft para 28 números significativos.

O SciPy (Scientific Python) provavelmente também pode lidar com cálculos financeiros (eu não tentei, mas suspeito que sim).

A GNU Multiple Precision Library (GMP) e a GNU MFPR Library são dois recursos livres e de código aberto para C e C ++.

Também existem bibliotecas de precisão numérica para JavaScript (!) E acho que o PHP pode lidar com cálculos financeiros.

Também existem soluções proprietárias (principalmente para Fortran) e de código aberto, bem como para muitas linguagens de computador.

Não sou cientista da computação treinando. No entanto, eu tendem a inclinar-se para BigDecimal em Java ou decimal em C #. Não tentei as outras soluções que listei, mas provavelmente também são muito boas.

Para mim, eu gosto do BigDecimal por causa dos métodos que ele suporta. O decimal do C # é muito bom, mas não tive a chance de trabalhar com ele tanto quanto gostaria. Faço cálculos científicos de interesse para mim no meu tempo livre e o BigDecimal parece funcionar muito bem porque posso definir a precisão dos meus números de ponto flutuante. A desvantagem do BigDecimal? Às vezes, pode ser lento, especialmente se você estiver usando o método de divisão.

Você pode, rapidamente, procurar nas bibliotecas gratuitas e proprietárias em C, C ++ e Fortran.

fishermanhat
fonte
1
Em relação ao SciPy / Numpy, a precisão fixa (ou seja, decimal.Decimal do Python) não é suportada ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Algumas funções não funcionarão corretamente com Decimal (isnan, por exemplo). O Pandas é baseado no Numpy e foi iniciado no AQR, um importante fundo de hedge quantitativo. Então você tem sua resposta em relação aos cálculos financeiros (não à contabilidade de supermercado).
comte
2

Para adicionar respostas anteriores, há também a opção de implementar o Joda-Money em Java, além do BigDecimal, ao lidar com o problema abordado na pergunta. O nome do módulo Java é org.joda.money.

Requer o Java SE 8 ou posterior e não possui dependências.

Para ser mais preciso, há dependência em tempo de compilação, mas não é necessária.

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

Exemplos de uso do 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();

Documentação: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Exemplos de implementação: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

Tadija Malić
fonte
0

Algum exemplo ... isso funciona (na verdade não funciona como o esperado), em quase qualquer linguagem de programação ... Eu tentei com Delphi, VBScript, Visual Basic, JavaScript e agora com 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!!!");
    }

RESULTADO:

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

WilliamK
fonte
3
O problema não é que o erro de arredondamento ocorra, mas que você não lida com ele. Arredonde o resultado para duas casas decimais (se você quiser centavos) e pronto.
Maaartinus 18/0317
0

Float é uma forma binária de decimal com design diferente; são duas coisas diferentes. Existem pequenos erros entre dois tipos quando convertidos um no outro. Além disso, o float é projetado para representar um número infinito de valores científicos. Isso significa que ele foi projetado para perder precisão para números extremamente pequenos e grandes com esse número fixo de bytes. Decimal não pode representar um número infinito de valores; ele limita apenas a esse número de dígitos decimais. Então Float e Decimal são para propósitos diferentes.

Existem algumas maneiras de gerenciar o erro do valor da moeda:

  1. Use inteiro longo e conte em centavos.

  2. Use precisão dupla, mantenha seus dígitos significativos em 15 apenas para que o decimal possa ser exatamente simulado. Rodada antes de apresentar valores; Arredonde frequentemente ao fazer cálculos.

  3. Use uma biblioteca decimal como Java BigDecimal para não precisar usar o dobro para simular o decimal.

ps: é interessante saber que a maioria das marcas de calculadoras científicas portáteis funciona em decimal em vez de em float. Portanto, nenhuma reclamação apresenta erros de conversão.

Chris Tsang
fonte