Posso encontrar muitas perguntas sobre bibliotecas a serem usadas para representar valores em alguma moeda. E sobre a questão antiga de por que você não deve armazenar moeda como um número de ponto flutuante IEEE 754. Mas não consigo encontrar mais nada. Certamente há muito mais a saber sobre moeda no uso no mundo real. Estou particularmente interessado no que você precisa saber para representá-lo em uso físico (por exemplo, com o dólar, você nunca tem precisão inferior a US $ 0,01, permitindo a representação como um número inteiro de centavos).
Mas é difícil fazer suposições sobre a versatilidade do seu programa quando as únicas moedas que você conhece são as populares ocidentais (como dólar, euro e libra). O que mais é conhecimento relevante em uma perspectiva puramente programática? Não estou preocupado com o tópico da conversão.
Em particular, o que precisamos saber para poder armazenar valores em alguma moeda e imprimi-los?
Respostas:
Sério?
Sinta-se à vontade para armazenar polegadas nos números de ponto flutuante IEEE 754 . Eles armazenam exatamente como você esperaria.
Sinta-se à vontade para armazenar qualquer quantia de dinheiro nos números de ponto flutuante IEEE 754 que você pode armazenar usando os ticks que dividem uma régua em frações de polegada.
Por quê? Porque quando você usa o IEEE 754, é assim que você o armazena.
O problema das polegadas é que elas são divididas em duas partes. A coisa sobre a maioria dos tipos de moeda é que eles são divididos em décimos (alguns tipos não são, mas vamos manter o foco).
Essa diferença não seria tão confusa, exceto que, para a maioria das linguagens de programação, a entrada e saída dos números de ponto flutuante IEEE 754 é expressa em decimais! O que é muito estranho, porque eles não são armazenados em decimais.
Por causa disso, você nunca consegue ver como os bits fazem coisas estranhas quando você pede ao computador para armazenar
0.1
. Você só vê a estranheza quando faz contas contra ela e ela apresenta erros estranhos.Do java eficaz de Josh Bloch :
Produz
0.6100000000000001
O que é mais revelador sobre isso não é o
1
caminho para a direita. São os números estranhos que precisavam ser usados para obtê-lo. Em vez de usar o exemplo mais popular,0.1
precisamos usar um exemplo que mostre o problema e evite o arredondamento que o ocultaria.Por exemplo, por que isso funciona?
Produz
-0.01
Porque nós tivemos sorte.
Eu odeio problemas difíceis de diagnosticar porque às vezes tenho "sorte".
O IEEE 754 simplesmente não pode armazenar 0,1 com precisão. Mas se você pedir para armazenar 0,1 e depois imprimir, será exibido 0,1 e você achará que está tudo bem. Não está bom, mas você não pode ver isso porque está arredondando para voltar a 0,1.
Algumas pessoas confundem os outros chamando essas discrepâncias de arredondamento de erros. Não, estes não são erros de arredondamento. O arredondamento está fazendo o que deveria e transformando o que não é decimal em decimal, para que possa ser impresso na tela.
Mas isso oculta a incompatibilidade entre como o número é exibido e como é armazenado. O erro não ocorreu quando o arredondamento aconteceu. Aconteceu quando você decidiu inserir um número em um sistema que não pode armazená-lo com precisão e supôs que ele estava sendo armazenado com precisão quando não estava.
Ninguém espera que π seja armazenado com precisão em uma calculadora e eles conseguem trabalhar com ela muito bem. Portanto, o problema não é sequer sobre precisão. É sobre a precisão esperada. Os computadores exibem um décimo
0.1
da mesma forma que nossas calculadoras, portanto esperamos que eles armazenem um décimo perfeitamente da maneira que nossas calculadoras. Eles não. O que é surpreendente, já que os computadores são mais caros.Deixe-me mostrar a incompatibilidade:
Observe que 1/2 e 0,5 se alinham perfeitamente. Mas 0.1 simplesmente não se alinha. Claro que você pode se aproximar se continuar dividindo por 2, mas nunca acertará exatamente. E precisamos de mais e mais bits cada vez que dividimos por 2. Portanto, representar 0,1 com qualquer sistema que divida por 2 precisa de um número infinito de bits. Meu disco rígido não é tão grande assim.
Portanto, o IEEE 754 para de tentar quando fica sem bits. O que é legal porque preciso de espaço no meu disco rígido para ... fotos de família. Não mesmo. Fotos de família. : P
De qualquer forma, o que você digita e o que vê são decimais (à direita), mas o que você armazena são bicimais (à esquerda). Às vezes, esses são perfeitamente iguais. Às vezes não são. Às vezes, parece que são iguais quando simplesmente não são. Esse é o arredondamento.
Por favor, se você estiver lidando com meu dinheiro com base decimal, não use carros alegóricos ou duplos.
Se você tem certeza de que coisas como décimos de centavos não estarão envolvidas, basta guardar centavos. Caso contrário, descubra qual será a menor unidade dessa moeda e use-a. Se não puder, use algo como BigDecimal .
Meu patrimônio líquido provavelmente sempre se encaixará perfeitamente em um número inteiro de 64 bits, mas coisas como o BigInteger funcionam bem em projetos maiores que isso. Eles são apenas mais lentos que os tipos nativos.
Descobrir como armazenar é apenas metade do problema. Lembre-se de que você também deve poder exibi-lo. Um bom design separará essas duas coisas. O verdadeiro problema com o uso de carros alegóricos aqui é que essas duas coisas são misturadas.
fonte
"Bibliotecas" são desnecessárias, a menos que a biblioteca padrão do seu idioma esteja ausente em certos tipos de dados, como explicarei.
Simplesmente, você precisa de ponto decimal fixo , e não ponto decimal flutuante . Por exemplo, a classe BigDecimal do Java pode ser usada para armazenar uma quantia em moeda. Outras linguagens modernas têm tipos semelhantes embutidos, incluindo C # e Python . As implementações variam, mas geralmente armazenam um número como um número inteiro, com o local decimal como um membro de dados separado. Isso fornece precisão exata, mesmo ao executar aritmética que daria restos ímpares (por exemplo, 0,0000001) com números de ponto flutuante IEEE.
Existem alguns pontos importantes.
Use um tipo decimal real em vez de ponto flutuante.
Entenda que o valor da moeda possui dois componentes: um valor (5,63) e um código ou tipo de moeda (USD, CAD, GBP, EUR, et al). Às vezes você pode ignorar o código da moeda, outras vezes é vital. E se você estiver trabalhando em um sistema financeiro ou de varejo / comércio eletrônico que permita várias moedas? O que acontece se você estiver tentando receber dinheiro de um cliente em CAD, mas ele deseja pagar com MXN? Você precisa de um tipo de "dinheiro" com um código de moeda e um valor de moeda para poder misturar esses valores (também taxa de câmbio, mas não quero ir muito longe na tangente). Ao mesmo tempo, meu software de finanças pessoais nunca precisa se preocupar com isso, porque tudo está em dólares americanos (ele pode misturar moedas, mas eu nunca preciso).
Enquanto uma moeda pode ter uma unidade física menor na prática (CAD e USD têm centavos, o JPY é apenas ... Yen), é possível ficar menor. A resposta de CandiedOrange aponta os preços dos combustíveis em décimos de centavo. Meus impostos sobre a propriedade são avaliados como moinhos por dólar ou décimos de centavo (1/1000 de um dólar americano). Não se limite a US $ 0,01. Embora você possa exibir esses valores na maioria das vezes, seus tipos devem permitir menores (os tipos decimais mencionados acima permitem).
Cálculos intermediários certamente devem permitir mais precisão do que um único centavo. Trabalhei em sistemas de varejo / comércio eletrônico em que os valores internos foram arredondados para US $ 0,00000001 internamente. Normalmente, a precisão infinita não é suportada por tipos decimais (ou SQL); portanto, deve haver algum limite. Por exemplo, dividir 1/3 usando o BigDecimal do Java lançará uma exceção sem um RoundingMode ou MathContext especificado, porque o valor não pode ser representado exatamente.
De qualquer forma, isso é crítico em certos casos. Vamos supor que você tenha um usuário com seis itens no carrinho de compras e ele fará o check-out. Seu sistema precisa calcular impostos e faz isso por item, pois os itens podem ser tributados de maneira diferente. Se você arredondar os impostos para cada item, poderá receber erros de arredondamento de um centavo no nível da transação / carrinho. Uma abordagem para corrigir isso pode ser armazenar impostos em mais casas decimais por item, obter o total de toda a transação e voltar e arredondar cada item para que o imposto total esteja correto (talvez um item seja arredondado para cima, outro para baixo).
O importante a se perceber com tudo isso é que algo tão importante quanto arredondar moedas de um centavo pode ser muito importante para as pessoas certas (por exemplo, alguns de meus clientes anteriores que tiveram que pagar os impostos governamentais sobre vendas em nome de seus clientes). No entanto, todos esses são problemas resolvidos. Lembre-se dos pontos acima e faça algumas experiências por conta própria, e você aprenderá fazendo isso.
fonte
Um lugar onde muitos desenvolvedores são confrontados com uma única representação de dados para qualquer moeda é para compras no aplicativo para aplicativos iOS. Seu cliente pode estar conectado a uma loja em quase qualquer país do mundo. E nessa situação, você receberá um preço de compra que consiste em um número de precisão dupla e um código de moeda.
Você precisa estar ciente de que os números podem ser grandes. Existem moedas em que o equivalente a, digamos, dez dólares, é superior a 100.000 unidades. E temos sorte de não haver moedas como o dólar do Zimbábue no momento, onde você pode ter notas de cem trilhões de dólares!
Para exibir moedas, você precisará de uma biblioteca - você não tem chance de fazer tudo isso sozinho. A exibição depende de duas coisas: o código da moeda e a localidade do usuário. Pense em como os dólares americanos e os canadenses seriam exibidos com um código de idioma dos EUA e um código de idioma canadense: nos EUA, você tem $ vs CAN $ e no Canadá você tem US $ vs $. Espero que esteja embutido no sistema operacional ou você tenha uma boa biblioteca.
Para cálculos, qualquer cálculo terminará com uma etapa de arredondamento. Você terá que descobrir como deve executar esse arredondamento legalmente . Isso não é um problema de programação, é um problema legal. Por exemplo, se você calcular o IVA no Reino Unido, precisará calcular o imposto por item ou por linha de item e arredondá-lo para centavos. O que você arredondar depende da moeda. Mas as regras obviamente dependem do país. Você não pode esperar que um cálculo legalmente correto no Reino Unido seja legalmente correto no Japão e vice-versa.
fonte
Alguns exemplos de problemas relacionados ao código do idioma:
Outro problema em potencial é que, mesmo que você armazene a moeda corretamente no número de ponto fixo, ela pode precisar ser convertida em número de ponto flutuante, porque a biblioteca usada para impressão suporta apenas ponto flutuante.
fonte