Devo usar NSDecimalNumber para lidar com dinheiro?

Quando comecei a codificar meu primeiro aplicativo, usei o NSNumber para valores monetários sem pensar duas vezes. Então pensei que talvez os tipos fossem suficientes para lidar com meus valores. No entanto, fui aconselhado no fórum do iPhone SDK a usar o NSDecimalNumber, devido às suas excelentes capacidades de arredondamento.

Não sendo um matemático por temperamento, achei que o paradigma da mantissa / expoente poderia ser um exagero; Ainda, googlin ‘ao redor, percebi que a maioria das conversas sobre dinheiro / moeda no cacau foram encaminhados para NSDecimalNumber.

Observe que o aplicativo em que estou trabalhando será internacionalizado, portanto, a opção de contar o valor em centavos não é realmente viável, pois a estrutura monetária depende muito do local usado.

Tenho 90% de certeza de que preciso ir com NSDecimalNumber, mas como não encontrei nenhuma resposta inequívoca na Web (algo como: “se você lida com dinheiro, use NSDecimalNumber!”) Eu pensei em perguntar aqui. Talvez a resposta seja óbvia para a maioria, mas eu quero ter certeza antes de iniciar um re-fatoramento maciço do meu aplicativo.

Me convença 🙂

Marcus Zarra tem uma postura bastante clara sobre isso: “Se você está lidando com moeda, então você deve estar usando NSDecimalNumber.” Seu artigo me inspirou a olhar para o NSDecimalNumber, e fiquei muito impressionado com isso. Erros de ponto flutuante IEEE ao lidar com a matemática base-10 tem me irritado por um tempo (1 * (0.5 – 0.4 – 0.1) = -0.00000000000000002776) e NSDecimalNumber acaba com eles.

NSDecimalNumber não apenas adiciona outros poucos dígitos de precisão binária de ponto flutuante, ele realmente faz a matemática de base-10. Isso elimina os erros como o mostrado no exemplo acima.

Agora, estou escrevendo um aplicativo de matemática simbólico, então meu desejo por mais de 30 dígitos de precisão decimal e nenhum erro de ponto flutuante pode ser uma exceção, mas acho que vale a pena olhar. As operações são um pouco mais complicadas do que a simples matemática do estilo var = 1 + 2, mas elas ainda são gerenciáveis. Se você estiver preocupado com a alocação de todos os tipos de instâncias durante suas operações matemáticas, o NSDecimal é o equivalente a C struct de NSDecimalNumber e existem funções C para fazer exatamente as mesmas operações matemáticas com ele. Na minha experiência, eles são muito rápidos para todos, menos para as aplicações mais exigentes (3.344.593 adições / s, 254.017 divisões / s em um MacBook Air, 281.555 adições / s, 12.027 divisões / s em um iPhone).

Como um bônus adicional, o método descriptionWithLocale: do NSDecimalNumber fornece uma string com uma versão localizada do número, incluindo o separador decimal correto. O mesmo acontece ao contrário do método initWithString: locale:.

Sim. Você tem que usar

NSDecimalNumber e

não dobrar ou flutuar quando você lida com moeda no iOS.

Por que é que??

Porque não queremos conseguir coisas como $ 9.9999999998 em vez de $ 10

Como isso acontece?

Floats e duplas são aproximações. Eles sempre vem com um erro de arredondamento. O formato que os computadores usam para armazenar decimais causa esse erro de desvio. Se você precisar de mais detalhes, leia

http://floating-point-gui.de/

De acordo com a apple docs,

NSDecimalNumber é uma subclass imutável de NSNumber, fornece um wrapper orientado a object para fazer aritmética de base 10. Uma instância pode representar qualquer número que possa ser expresso como mantissa x 10 ^ expoente onde mantissa é um inteiro decimal de até 38 dígitos e o expoente é um inteiro de –128 a 127.wrapper para fazer a aritmética de base 10.

Por isso, NSDecimalNumber é recomendado para lidar com a moeda.

(Adaptado do meu comentário sobre a outra resposta)

Sim você deveria. Um número inteiro de centavos funciona apenas enquanto você não precisa representar, digamos, meio centavo. Se isso acontecer, você pode alterá-lo para contar meio centavo, mas e se você precisar representar um quarto de centavo ou um oitavo de centavo?

A única solução adequada é NSDecimalNumber (ou algo parecido), que coloca o problema em 10 ^ -128 ¢ (ou seja,
0.000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000001).

(Outra forma seria aritmética de precisão arbitrária, mas isso requer uma biblioteca separada, como a biblioteca GNU MP Bignum . O GMP está sob a LGPL. Eu nunca usei essa biblioteca e não sei exatamente como ela funciona, então eu não poderia dizer o quão bem isso funcionaria para você.)

[Edit: Aparentemente, pelo menos uma pessoa – Brad Larson – acha que estou falando sobre ponto flutuante binário em algum lugar nesta resposta. Eu não estou.]

Uma pergunta melhor é, quando você não deve usar NSDecimalNumber para lidar com dinheiro. A resposta curta para essa pergunta é: quando você não pode tolerar a sobrecarga de desempenho de NSDecimalNumber e não se importa com pequenos erros de arredondamento, porque você nunca está lidando com mais do que alguns dígitos de precisão. A resposta ainda mais curta é que você deve sempre usar o NSDecimalNumber ao lidar com dinheiro.

Eu achei conveniente usar um inteiro para representar o número de centavos e depois dividir por 100 para apresentação. Evita todo o problema.

VISA, MasterCards e outros estão usando valores inteiros ao passar valores. Cabe ao remetente e ao receptor analisar corretamente os valores de acordo com o expoente da moeda (dividir ou multiplicar por 10 ^ num, em que num – é um expoente da moeda). Note que diferentes moedas têm diferentes expoentes. Normalmente é 2 (por isso dividimos e multiplicamos por 100), mas algumas moedas têm expoente = 0 (VND, etc) ou = 3.