18 de fevereiro de 2013

Cálculos com dinheiro

Em Python, podemos encontrar alguns problemas de arredondamento se usarmos o tipo float, principalmente quando fazemos contas com valores monetários.

Por exemplo:
>>> 1.23 + 1.01
2.24
>>> n = 1.23 + 2.01
>>> print n # muitas casas decimais!
3.2399999999999998
>>> '{0:,.2f}'.format(n) # arredondou na hora de mostrar :-(
3.24
>>> 

Como fazer essa conta acima considerando apenas 2 casas decimais e dando o resultado correto, que é 3.24? Vamos pedir ajuda ao módulo decimal. Existem algumas dicas práticas de como usá-lo, como podemos ver abaixo.

Eu já tenho os dados com as casas decimais que preciso

Considerando literalmente as casas decimais já existentes, sem nenhuma conversão:
>>> a = 1.23
>>> b = 2.01
>>> from decimal import *
>>> da = Decimal(str(a))
>>> db = Decimal(str(b))
>>> da
Decimal('1.23')
>>> db
Decimal('2.01')
>>> a + b
Decimal('3.24')
>>> 

Essa é a 1ª dica: se você já tem os dados na quantidade de casas decimais desejada, crie seu Decimal convertendo o float para string.

Veja o que aconteceria se não fizéssemos essa conversão:
>>> from decimal import *
>>> Decimal(1.23)
Decimal('1.229999999999999982236431605997495353221893310546875')
>>> 

Aí o resultado final ficaria, novamente, uma bagunça!

Eu tenho os dados com quantidades diferentes de casas decimais


O primeiro passo é normalizar os dados, desprezando as casas decimais excedentes, e depois fazer o cálculo normalmente.
>>> a = 1.23789
>>> b = 2.01
>>> from decimal import *
>>> da = Decimal(str(a)).quantize(Decimal('1.00'), rounding=ROUND_DOWN)
>>> db = Decimal(str(b))
>>> da
Decimal('1.23')
>>> db
Decimal('2.01')
>>> a + b
Decimal('3.24')
>>> 

A mágica, aqui é o quantize() que considera 2 casas decimais (os dois zeros depois do ponto decimal) com o arredondamento para baixo, que na verdade, não arredonda nada. Sugiro que você faça esse teste sem informar o parâmetro rounding e veja que o número será arredondado para cima, o que não queremos que aconteça.

Eu preciso fazer os cálculos com mais casas, mas apresentar com apenas duas, sem arredondar


O princípio, nesse caso, é bem parecido com o que vimos acima. Vamos usar como exemplo as contas de um posto de combustíveis.

O valor do litro tem 3 casas, mas temos que pagar com apenas 2. Assim, se eu abasteço várias vezes num mesmo posto e vou pagar no final do mês, os abastecimentos precisam ser guardados e somados com 3 casas. Só o valor final é que deve ser representado com 2 casas, sem arredondamento.

Vamos ao exemplo:
>>> a = 10.239
>>> b = 50.498
>>> c = 20.159
>>> from decimal import *
>>> da = Decimal(str(a))
>>> db = Decimal(str(b))
>>> dc = Decimal(str(c))
>>> da
Decimal('10.239')
>>> db
Decimal('50.498')
>>> dc
Decimal('20.159')
>>> da + db + dc
Decimal('80.896')
>>> final = (da+db+dc).quantize(Decimal('1.00'), rounding=ROUND_DOWN)
>>> final
Decimal('80.89')
>>> 

Novamente, a mágica está no quantize(). Experimente fazer a mesma conta sem o rounding e veja que o resultado é diferente.

Para mais informações a respeito do módulo decimal, eis a documentação oficial: Decimal fixed point and floating point arithmetic.

Se você tiver uma outra maneira (mais prática ou mais segura) de resolver esses problemas, contribua nos comentários. Sugestões são bem vindas.

Eu sou Vinicius Assef, um programador do século passado que gosta de Python, pratica Lean Development e acredita em Deus. Você pode me contactar por email ou twitter.

2 comentários:

  1. Parabéns pelo post Vinícius, bem bacana essa ideia ensinando como manipular 'dinheiro', tem horas que é confuso e chegamos a criar diveros pogs para que a saída venha bacana.

    ResponderExcluir
  2. Muito bom o post! Esse é um assunto que, vira e mexe, provoca alguma dor de cabeça, seja em Python ou em qualquer outra linguagem. Valeu!

    ResponderExcluir

Marcadores