Pular para o conteúdo

Número exato

Arcane.Decimal — para quando 0,1 + 0,2 precisa dar 0,3: dinheiro, imposto, e todo número que alguém confere na mão.

dataforge
out 0.1 + 0.2        # 0.30000000000000004

Float é IEEE 754 de 64 bits, e ele não representa 0,1. Representa o binário mais próximo, e a diferença aparece na soma.

Isso não é defeito do DataForge — é assim em toda linguagem com ponto flutuante binário, e é o certo para física, estatística e gráficos, onde a precisão relativa importa mais que o dígito decimal exato.

A resposta: o sufixo `d`#

dataforge
assert 0.1 + 0.2 is not 0.3          // Float: o classico
assert 0.1d + 0.2d is 0.3d           // Decimal: exato

preco := 19.99d
out typeof(preco)                    // Decimal
out preco * 3                        // 59.97

O sufixo `d` faz o literal nascer Decimal, construído a partir do texto. É a diferença que decide: Decimal("0.1") é exato, e Decimal(0.1) já carrega o erro do float que veio antes dele.

Antes do sufixo, a exatidão exigia escrever Dec.de("19.99") — e quem escrevia 19.99 não era avisado de nada. O literal e o módulo produzem o mesmo valor:

dataforge
adopt Arcane.Decimal as Dec

assert 19.99d is Dec.de("19.99")
assert Dec.e_decimal(19.99d) is yes
assert Dec.centavos(19.99d) is 1999
out "o literal e Dec.de sao o mesmo valor"

O módulo, para o resto#

dataforge
adopt Arcane.Decimal as Dec

a := Dec.de("0.1")
b := Dec.de("0.2")
out a + b            # 0.3

Decimal guarda o número em base dez, do jeito que foi escrito. A aritmética é a da própria linguagem — +, -, *, /, bigger, is funcionam — porque o interpretador trata valor por protocolo, e não por tipo.

dataforge
preco := Dec.de("19.99")
out preco * 3                 # 59.97
out typeof(preco)             # Decimal
out max([Dec.de("3.10"), Dec.de("1.05")])   # 3.10

Arredondar é meio-para-cima#

dataforge
out Dec.arredondar(Dec.de("2.5"))    # 3
out round(2.5)                       # 2.0

O round embutido faz arredondamento bancário: 0,5 vira 0 e 2,5 vira 2. É o certo para estatística, porque não enviesa uma série longa.

Para dinheiro é errado: 2,5 centavos precisam virar 3, sempre, ou o cliente reclama do extrato. É a mesma decisão que o pacote moeda já tinha tomado.

Os outros modos continuam à mão: MEIO_PAR, MEIO_PARA_BAIXO, CIMA, BAIXO, TRUNCA, LONGE_DO_ZERO.

Repartir sem perder centavo#

dataforge
partes := Dec.repartir(Dec.de("10.00"), 3)
out partes                   # [3.34, 3.33, 3.33]
out Dec.soma(partes)         # 10.00

Três vezes 3,33 são 9,99, e o décimo real sumiu. Dividir dinheiro em partes iguais quase nunca dá partes iguais — o resto é distribuído um centavo por parte, que é como uma conta é rateada de verdade.

Misturar exato com aproximado é recusado#

text
erro[DF0301]: '+' entre um Decimal e um Float e recusado.
   = nota: a esquerda e exata e a direita e aproximada; o resultado
           seria aproximado, e a garantia se perderia sem aviso
   = dica: converta o lado que falta:
       Decimal.de(x) + exato       para seguir exato
       Decimal.float(exato) + x    para aceitar o float

Somar um número exato com um aproximado devolve um aproximado. A recusa é deliberada: sem ela, a garantia que se veio buscar desapareceria em silêncio, e o erro só apareceria três somas depois.

Com inteiro funciona sem conversão — inteiro é exato, então não há nada a perder.

`Dec.de(0.1)` devolve `0.1`#

E não 0.1000000000000000055511151231257827…, que é o binário cru. A conversão passa pelo texto do número: é o que a pessoa escreveu e o que ela quer dizer. Converter o binário seria tecnicamente mais fiel e praticamente inútil — ninguém digita 0.1 querendo o binário mais próximo dele.

A tabela inteira#

Faz
Dec.de(x)texto, inteiro ou float viram exato — aceita vírgula
Dec.de_centavos(1999)19.99 — a ponte com quem guarda dinheiro em inteiro
Dec.zero()o zero exato
Dec.texto(d, casas)como texto, arredondando se casas vier
Dec.float(d) / Dec.inteiro(d)de volta — a exatidão acaba aqui
Dec.centavos(d)19.99 vira 1999
Dec.arredondar(d, casas, modo)meio-para-cima por padrão
Dec.soma(xs) / Dec.media(xs)sobre um cluster, exato
Dec.repartir(d, partes, casas)divide sem perder centavo
Dec.abs(d) / Dec.sinal(d)valor absoluto e −1/0/1
Dec.casas(d) / Dec.e_decimal(x) / Dec.modos()perguntar

Quando não usar#

Para física, estatística, geometria e gráficos, use Float. Decimal é mais lento, e a exatidão decimal não é a garantia que esses domínios precisam — ali o que importa é precisão relativa, e o ponto flutuante binário é a ferramenta certa.

Para dinheiro guardado em banco de dados, considere também o pacote `moeda`, que trabalha em centavos inteiros e traz a formatação em BRL.