Valores decimais¶
Nenhum valor fiscal desta biblioteca passa por float64. Essa é uma decisão de
projeto, não uma preferência estética.
O problema com ponto flutuante¶
float64 representa números em base 2. Valores como 0,1 e 0,07 não têm
representação exata nessa base, e o erro se acumula:
Sete reais viram sete reais e uma fração invisível. Quando esse valor é formatado com duas casas o erro some, mas quando ele participa de uma comparação, de uma diferença ou de mais uma multiplicação, o centavo aparece — e a SEFAZ rejeita a nota por soma que não fecha.
Com tipos.Decimal
o mesmo laço dá exatamente 7.00.
Como funciona¶
Decimal guarda um inteiro sem escala e a quantidade de casas decimais. O valor
10,50 é o par (1050, 2). Todas as operações são feitas em inteiros, então
não há erro de representação.
a := tipos.D("10.50") // 1050 com 2 casas
b := tipos.D("0.255") // 255 com 3 casas
a.Somar(b) // 10.755 — assume a maior escala
a.Subtrair(b) // 10.245
a.Multiplicar(b) // 2.67750 — escalas se somam
a.MultiplicarCom(b, 2) // 2.68 — arredondado para 2 casas
a.Percentual(tipos.D("18.00"), 2) // 1.89 — 18% de 10,50
Construindo valores¶
| Função | Uso |
|---|---|
tipos.D("199.90") |
Literais no código; entra em pânico se o texto for inválido |
tipos.ParseDecimal(s) |
Entrada externa, devolve erro |
tipos.ParseDecimalBR(s) |
Formato brasileiro: "1.234,56" |
tipos.NovoDecimal(19990, 2) |
A partir de centavos, quando o banco de dados guarda inteiros |
tipos.DeInteiro(42) |
Inteiro sem casas decimais |
A escala é preservada exatamente como escrita: tipos.D("10.50") tem duas
casas, tipos.D("10.5") tem uma, e as duas são numericamente iguais para
Igual e Comparar.
Nunca converta de float64
Não existe construtor a partir de float64, e isso é de propósito. Se o
valor chega como float64 de um sistema que você não controla, formate-o
com a precisão desejada antes de converter:
Float64() existe para o caminho inverso — exibição e cálculos aproximados
— e nunca deve alimentar um XML fiscal.
Escala automática pelo leiaute¶
Cada campo do leiaute tem uma precisão obrigatória: vProd tem duas casas,
qCom tem quatro, vUnCom tem dez. Escrever <vProd>25</vProd> é rejeição na
certa; o correto é <vProd>25.00</vProd>.
Você não precisa se preocupar com isso. As estruturas do pacote nfe declaram a
precisão de cada campo em uma tag, e NFe.Preparar reescala tudo antes de
serializar:
type Prod struct {
QCom tipos.Decimal `xml:"qCom" dec:"4"`
VUnCom tipos.Decimal `xml:"vUnCom" dec:"10"`
VProd tipos.Decimal `xml:"vProd" dec:"2"`
}
prod.QCom = tipos.D("10") // você escreve assim
// depois de Preparar, o XML sai <qCom>10.0000</qCom>
Arredondamento¶
O arredondamento é comercial: metade para longe do zero, que é o critério adotado pelos manuais da SEFAZ.
tipos.D("2.345").ComCasas(2) // 2.35
tipos.D("2.344").ComCasas(2) // 2.34
tipos.D("-2.345").ComCasas(2) // -2.35
tipos.D("10.5").ComCasas(0) // 11
Isso difere do arredondamento bancário (metade para o par) usado por várias linguagens, e da truncagem. Se o seu ERP arredonda de outro jeito, os totais podem divergir em um centavo — a validação da biblioteca tolera essa diferença por item, como o próprio leiaute faz.
Arredondar por item, somar depois¶
A ordem das operações importa e é fixada pelo leiaute: o tributo é calculado e arredondado por item, e o total é a soma dos valores já arredondados.
// Certo: arredonda cada item, depois soma.
item1 := tipos.D("25.00").Percentual(tipos.D("1.65"), 2) // 0.41
item2 := tipos.D("74.70").Percentual(tipos.D("1.65"), 2) // 1.23
total := item1.Somar(item2) // 1.64
// Errado: soma as bases e arredonda no fim.
tipos.D("99.70").Percentual(tipos.D("1.65"), 2) // 1.65
Um centavo de diferença. CalcularTotais faz do jeito certo.
Serialização¶
Decimal implementa as interfaces de XML e de JSON. Em JSON o valor sai como
string, para não perder precisão ao passar por um parser que use float64:
Na leitura, tanto string quanto número JSON são aceitos.