Skip to main content

Command Palette

Search for a command to run...

Como tratar valores monetários em JavaScript

Não há consenso na programação sobre como tratar valores monetários

Updated
7 min read

Tradução livre do post “How to Handle Monetary Values in JavaScript” originalmente publicado por Sarah Dayan em 13 de Abril de 2018.

~

O dinheiro está em todo lugar. Aplicativos bancários, sites de comércio eletrônico, plataformas de bolsa de valores, interagimos diariamente com dinheiro. Também dependemos cada vez mais da tecnologia para lidar com o nosso.

No entanto, não há consenso sobre como lidar na programação com valores monetários. É um conceito predominante nas sociedades modernas, mas não é um tipo primitivo em nenhuma linguagem convencional, enquanto coisas como data e hora são. Como resultado, todo software vem com sua própria maneira de lidar com dinheiro, com todos os problemas que o acompanham.

Problema #1: Dinheiro como Number

Seu primeiro instinto quando você precisa representar dinheiro pode ser usar Number. Dinheiro não passa de um valor numérico, certo? Errado.

Uma parte de um valor monetário é relativa apenas a outro aspecto: sua moeda. Não existe 10 “dinheiro”. São 10 dólares, 10 euros, 10 bitcoins … Se você deseja somar dois valores monetários com moedas diferentes, é necessário convertê-los primeiro. O mesmo se você quiser compará-los: se tudo o que você tem é uma quantia, não é possível fazer uma comparação precisa. Valor e moeda são inseparáveis.

Problema #2: Matemática de ponto flutuante

A maioria das moedas contemporâneas é decimal ou não possui subunidades. Isso significa que quando o dinheiro tem subunidades, o número delas em uma unidade principal é uma potência de 10. Por exemplo, existem 100 centavos em um dólar, sendo 10 na potência de 2.

O uso de um sistema decimal tem vantagens, mas levanta uma questão importante quando se trata de programação. Os computadores usam um sistema binário, portanto não podem representar números decimais de forma nativa. Algumas linguagens criaram suas próprias soluções, como o tipo BigDecimal em Java ou o decimal em C#. O JavaScript possui apenas o tipo Number, que pode ser usado como um número inteiro ou como float. Por ser uma representação binária de um sistema base 10, você acaba com resultados imprecisos ao tentar fazer cálculos.

0.1 + 0.2 // retorna 0.30000000000000004 😧

Usar floats para armazenar valores monetários é uma má ideia. Conforme você calcula mais valores, os erros imperceptíveis de precisão levam a intervalos maiores. Isso inevitavelmente acaba causando problemas de arredondamento.

Problema #3: porcentagem vs. divisão

Às vezes, é necessário dividir o dinheiro, mas as porcentagens não podem ser reduzidas sem adicionar ou perder centavos.

Imagine que você precisa cobrar R$ 999,99 com um adiantamento de 50%. Isso pode ser feito com algumas contas simples. A metade é de R$ 499,995, mas você não pode dividir um centavo e provavelmente arredondará o resultado para R$ 500. O problema é que, ao cobrar a segunda metade, você acaba com o mesmo resultado e cobra um centavo extra.

Você não pode confiar apenas em porcentagens ou divisões para dividir dinheiro, porque não é divisível ao infinito. O preço do gás pode mostrar mais de dois dígitos da fração, mas é apenas simbólico: você sempre acaba pagando um preço arredondado.

A Engenharia correta

Como você pode ver, há muito mais em dinheiro do que aparenta e é mais do que o simples tipo de dado Number podem suportar.

Felizmente, o engenheiro de software Martin Fowler apresentou uma solução. Em Patterns of Enterprise Application Architecture, ele descreve um padrão para valores monetários:

Propriedades

  • valor;
  • moeda.

Métodos

  • math: adicionar, subtrair, multiplicar, dividir;
  • comparação: igual a, maior que, maior que ou igual, menor que, menor que ou igual.

A partir disso, você pode criar objetos de valor que atendam à maioria das suas necessidades monetárias.

Dinheiro como estrutura de dados

O dinheiro se comporta de maneira diferente de um número simples e, portanto, deve ser tratado de maneira diferente. A primeira e mais importante é que ele sempre deve ser composto por um valor e uma moeda.

Você pode fazer tudo, desde que tenha um valor e uma moeda. Você pode adicionar valores monetários, verificar se são iguais ou não, formatá-los para o que você precisar. Isso pode ser feito através dos métodos de um objeto. Em JavaScript, qualquer tipo de função que retorne um objeto fará esta proeza.

Valores em centavos

Existem várias maneiras de resolver o problema de float no JavaScript.

Você pode usar bibliotecas como Decimal.js que manipularão seus floats como strings. Esta não é uma solução ruim e é útil quando você precisa lidar com grandes números. No entanto, isso custa à adição de uma dependência (pesada) e, consequentemente, um pior desempenho.

Você pode multiplicar floats com números inteiros antes de calcular e depois dividi-los novamente.

(0.2 * 100 + 0.01 * 100) / 100 // retorna 0.21 🎉

É uma boa solução, mas requer cálculos extras na construção do objeto ou em cada manipulação. Isso não é necessariamente má para a performance, mas ainda é mais trabalhoso do que o necessário.

Uma terceira alternativa é armazenar diretamente valores em centavos, em relação à unidade. Se você precisar armazenar 10 centavos, você não armazenará 0.1, mas 10. Isso permite que você trabalhe apenas com números inteiros, o que significa cálculos seguros (até atingir grandes números) e ótimo desempenho.

Dinero.js: uma biblioteca imutável para criar, calcular e formatar valores monetários

A partir dessas observações, criei uma biblioteca JavaScript: Dinero.js.

post-monetário.png

Dinero.js segue o padrão de Fowler e muito mais. Permite criar, calcular e formatar valores monetários em JavaScript. Você pode fazer contas, analisar e formatar seus objetos, fazer perguntas e facilitar o processo de desenvolvimento.

A biblioteca foi projetada para ser imutável e encadeada. Ela suporta configurações globais, amplia as opções de formatação e fornece suporte nativo à internacionalização.

Por que imutável?

Uma biblioteca imutável é mais segura e previsível. Operações mutáveis ​​e cópias de referência são as fontes de muitos bugs. Optar pela imutabilidade evita-os completamente.

Com o Dinero.js, você pode executar cálculos sem se preocupar em alterar instâncias originais. No exemplo a seguir usando Vue.js, price não será alterado quando priceWithTax for chamado. Se a instância fosse mutável, seria.

const vm = new Vue({
  data: {
    price: Dinero({ amount: 500 })
  },
  computed: {
    priceWithTax() {
      return this.price.add(this.price.percentage(10))
    }
  }
})

Encadeamento

Bons desenvolvedores se esforçam para tornar seu código mais conciso e fácil de ler. Quando você deseja executar várias operações sucessivamente em um único objeto, o encadeamento fornece uma notação elegante e sintaxe concisa.

Dinero({ amount: 500 })
   .add(Dinero({ amount: 200 }))   
   .multiply(4)
   .setLocale('fr-FR')
   .toFormat() // retorna "28,00 US$"

Configurações globais

Ao lidar com muitos valores monetários, é provável que você queira que alguns atributos sejam compartilhados. Se você estiver criando um site em alemão, provavelmente desejará mostrar valores com o formato da moeda alemã.

É aqui que as configurações globais são úteis. Em vez de passá-las para todas as instâncias, você pode declarar opções que serão aplicadas a todos os novos objetos.

Dinero.globalLocale = 'de-DE'
Dinero({ amount: 500 }).toFormat() // retorna "5,00 $"

Suporte nativo à internacionalização

Tradicionalmente, as bibliotecas usam arquivos de localidade para internacionalização. Se são extensíveis, elas tendem a tornar as bibliotecas muito mais pesadas.

internacio.png

Arquivos locais também são difíceis de manter. A API de internacionalização é nativa e muito bem suportada. A menos que você precise trabalhar com navegadores desatualizados e/ou irrelevantes, é seguro usar toFormat.

Formatação

Um objeto é ótimo para armazenar dados, mas não é tão útil quando se trata de exibi-los. O Dinero.js vem com vários métodos de formatação, incluindo toFormat. Ele fornece um syntactic sugar intuitivo e conciso para Number.prototype.toLocaleString. Associe-o a setLocale e você poderá exibir qualquer objeto Dinero no formato adequado, em qualquer idioma. Isso é particularmente útil para sites de comércio eletrônico multilíngues.

E agora?

O padrão monetário da Fowler é amplamente reconhecido como uma ótima solução. Ele inspirou muitas implementações em várias linguagens. Se você gosta do “faça você mesmo”, recomendo esse padrão e as observações deste artigo como ponto de partida. Ou você pode escolher o Dinero.js: uma solução moderna, confiável e totalmente testada que já funciona. Diverta-se!

Alguma pergunta sobre Dinero.js? Ou sobre como criar sua própria estrutura de dados monetários? Vamos conversar no Twitter !