Em quase toda linguagem, 0.1 + 0.2 não resulta em 0.3, e sim em 0.30000000000000004. Isso não é um defeito: números de ponto flutuante binário (o padrão IEEE 754) não conseguem representar exatamente a maioria das frações decimais. Em um gráfico, a diferença é irrelevante. Em um extrato, uma conciliação ou uma checagem de limite, ela vira centavos que aparecem ou somem sem explicação.
Regra zero: dinheiro não é float
Existem duas representações seguras para valores monetários:
- Inteiros na menor unidade da moeda. R$ 12,34 vira
1234centavos. Somas e comparações são exatas, e qualquer linguagem as executa rápido. - Tipos decimais de precisão definida.
Decimalem Python,BigDecimalem Java,rust_decimalem Rust,numericno PostgreSQL. Representam frações decimais exatamente e permitem controlar o arredondamento.
Inteiros são ótimos para valores finais em uma única moeda. Decimais são necessários quando há taxas, preços com mais casas que a moeda, conversões e cálculos de juros.
A menor unidade depende da moeda
Nem toda moeda tem duas casas decimais. A norma ISO 4217 define a quantidade de casas de cada uma: o real e o dólar têm 2, o iene japonês tem 0, e o dinar do Bahrein tem 3. Em cripto, tokens ERC-20 costumam usar 18 casas.
Por isso, um valor monetário nunca deve viajar sem a moeda. Um Money com valor e moeda evita somar reais com dólares e saber quantas casas usar ao arredondar.
Arredondamento é regra de negócio
Sempre que um cálculo produz mais casas do que a moeda permite, é preciso arredondar, e a regra escolhida muda o resultado:
| Valor | Meio para cima | Meio para o par (bancário) |
|---|---|---|
| 2,345 | 2,35 | 2,34 |
| 2,355 | 2,36 | 2,36 |
| 2,365 | 2,37 | 2,36 |
O arredondamento "meio para o par" evita o viés sistemático para cima quando muitos valores são arredondados, e é o padrão de várias bibliotecas. Mas a regra correta é a que o contrato, o regulador ou o parceiro exige. Ela deve ser explícita no código e coberta por testes, nunca herdada de um padrão implícito da linguagem.
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
CURRENCY_EXPONENT = {"BRL": 2, "USD": 2, "JPY": 0, "BHD": 3}
def quantize(amount: Decimal, currency: str, rounding=ROUND_HALF_EVEN) -> Decimal:
exp = Decimal(1).scaleb(-CURRENCY_EXPONENT[currency]) # 0.01 for BRL
return amount.quantize(exp, rounding=rounding)
fee = Decimal("1234.56") * Decimal("0.0175") # Decimal("21.604800"), exact
quantize(fee, "BRL") # Decimal('21.60')
quantize(Decimal("2.345"), "BRL") # Decimal('2.34') (half-even)
quantize(Decimal("2.345"), "BRL", ROUND_HALF_UP) # Decimal('2.35')Note que os valores são criados a partir de texto (Decimal("1234.56")). Criar um Decimal a partir de um float herda o erro binário que se queria evitar.
Dividir sem perder centavos
Dividir R$ 100,00 em três parcelas iguais dá 33,333... Arredondar cada parcela para 33,33 soma R$ 99,99, e um centavo desaparece. Em escala, esses centavos viram divergências de conciliação.
A solução é alocar: calcular as parcelas na menor unidade e distribuir o resto, um centavo por vez, de forma determinística.
def allocate(total_minor: int, weights: list[int]) -> list[int]:
"""Split an integer amount by weights; the parts always sum to the total."""
weight_sum = sum(weights)
shares = [total_minor * w // weight_sum for w in weights]
remainders = [total_minor * w % weight_sum for w in weights]
leftover = total_minor - sum(shares)
# Give the leftover cents to the largest remainders (ties: earliest first).
for i in sorted(range(len(weights)), key=lambda i: -remainders[i])[:leftover]:
shares[i] += 1
return shares
allocate(10_000, [1, 1, 1]) # [3334, 3333, 3333] -> sums to 10000
allocate(10_000, [70, 30]) # [7000, 3000]A mesma técnica serve para rateio de taxas entre participantes, distribuição de juros e divisão de uma ordem entre várias contas.
Fronteiras: banco, APIs e JSON
A precisão costuma se perder nas bordas do sistema:
- Banco de dados: use
numeric(p, s)ou inteiros. Colunasfloatoudouble precisionreintroduzem o problema. - JSON: números em JSON costumam ser lidos como
doubleem JavaScript, que só representa inteiros exatos até 2^53. Para valores grandes ou decimais, trafegue o valor como string e a moeda em um campo próprio. - Planilhas e relatórios: exportações para CSV devem formatar com a precisão da moeda, não com a representação interna do número.
{ "amount": "1234.56", "currency": "BRL" }Testes que valem a pena
- Valores na fronteira do arredondamento (x,xx5) para cada regra usada.
- Propriedade: a soma das parcelas alocadas é sempre igual ao total, para qualquer total e quaisquer pesos.
- Conversões ida e volta entre moedas não podem gerar valores com mais casas do que a moeda permite.
- Serialização e desserialização preservam o valor exato.
Resumo
Valores monetários exigem inteiros na menor unidade ou tipos decimais, sempre acompanhados da moeda. O arredondamento é uma regra de negócio explícita, a divisão de valores é feita por alocação para não perder centavos, e a precisão precisa ser preservada nas bordas: banco, APIs e exportações. São decisões pequenas que evitam as divergências mais difíceis de explicar em uma auditoria.
Leituras relacionadas
- Fintech · 4 min de leitura
- IA aplicada · 3 min de leitura
- IA aplicada · 4 min de leitura