Sempre me disseram para nunca representar dinheiro com double
ou com float
tipos, 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 é.
floating-point
currency
Fran Fitzpatrick
fonte
fonte
Respostas:
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
double
oufloat
provavelmente 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
BigDecimal
classe e C # tem odecimal
tipofonte
1.0 / 10 * 10
pode não ser o mesmo que 1.0.De Bloch, J., Effective Java, 2ª ed. Item 48:
Embora
BigDecimal
tenha algumas advertências (consulte a resposta atualmente aceita).fonte
long a = 104
e conta em centavos em vez de dólares.BigDecimal
.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.
fonte
Math.round(0.49999999999999994)
retornar 1?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,
resulta em
fonte
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:
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.
fonte
float
,double
eBigDecimal
sã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.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.
fonte
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.
fonte
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.
Resultado:
fonte
double
FP 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.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:
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 ::
Ao usar a API de implementação de referência, o código necessário é muito mais simples:
A API também suporta cálculos com MonetaryAmounts:
CurrencyUnit e MonetaryAmount
MonetaryAmount possui vários métodos que permitem acessar a moeda atribuída, o valor numérico, sua precisão e muito mais:
As MonetaryAmounts podem ser arredondadas usando um operador de arredondamento:
Ao trabalhar com coleções de MonetaryAmounts, estão disponíveis alguns métodos úteis de filtragem, classificação e agrupamento.
Operações personalizadas MonetaryAmount
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
fonte
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 .
fonte
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.
fonte
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.
fonte
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.
Exemplos de uso do Joda Money:
fonte
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:
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!!!
fonte
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:
Use inteiro longo e conte em centavos.
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.
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.
fonte