JavaScript, como muitas linguagens de programação, representa números em ponto flutuante de precisão dupla (IEEE 754). Isso significa que nem todos os números decimais podem ser representados exatamente em binário, o que leva a pequenas imprecisões em cálculos. Para quem trabalha com dinheiro, essas imprecisões podem se acumular e causar erros graves, como valores incorretos em transações financeiras.

Nesta aula, vamos explorar o clássico problema de 0.1 + 0.2, entender por que ele acontece e, principalmente, aprender estratégias práticas para lidar com valores monetários de forma segura em JavaScript. Vamos ver como trabalhar com centavos (inteiros) para evitar erros de arredondamento e também como usar o método toFixed com cuidado, pois ele pode introduzir novos problemas se usado incorretamente.

0.1+0.2

Abra o console do seu navegador e digite 0.1 + 0.2. O resultado será 0.30000000000000004, não 0.3. Isso acontece porque o computador armazena números em binário, e a fração decimal 0.1 não tem uma representação binária finita. Assim como 1/3 = 0.3333... em decimal, 0.1 em binário é uma dízima periódica: 0.00011001100110011... O ponto flutuante armazena apenas uma aproximação, e a soma de duas aproximações pode resultar em uma pequena diferença.

Esse erro é inerente à representação e não é um bug do JavaScript. Em linguagens como Python, o mesmo acontece. A questão é: como lidar com isso quando precisamos de resultados exatos, especialmente em cálculos financeiros?

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.3 - (0.1 + 0.2)); // 5.551115123125783e-17

Note que a diferença é extremamente pequena, mas ainda assim pode causar problemas em comparações diretas ou em acumulações de muitos cálculos. Por exemplo, se você somar 0.1 dez vezes, o resultado pode não ser exatamente 1.

let soma = 0;
for (let i = 0; i < 10; i++) {
    soma += 0.1;
}
console.log(soma); // 0.9999999999999999

Trabalhando com centavos

A abordagem mais comum e recomendada para lidar com dinheiro em JavaScript é trabalhar com a menor unidade monetária, geralmente centavos, representada como um inteiro. Isso elimina os problemas de ponto flutuante, pois inteiros são representados exatamente até um certo limite (2^53 - 1).

Em vez de armazenar R$ 19,99, você armazena 1999 (centavos). Todas as operações (soma, subtração, multiplicação, divisão) são feitas com inteiros, e apenas na exibição você converte para reais. Isso garante precisão exata.

Vamos ver um exemplo prático: um carrinho de compras que soma itens.

// Preços em centavos
const precos = [1999, 250, 4990]; // R$ 19,99; R$ 2,50; R$ 49,90
const totalCentavos = precos.reduce((acc, preco) => acc + preco, 0);
console.log(totalCentavos); // 7239

// Converter para reais para exibição
function formatarReais(centavos) {
    const reais = Math.floor(centavos / 100);
    const resto = centavos % 100;
    return `R$ ${reais},${resto.toString().padStart(2, '0')}`;
}
console.log(formatarReais(totalCentavos)); // R$ 72,39

Essa técnica é amplamente utilizada em sistemas financeiros reais. É importante também ter cuidado com a multiplicação e divisão: se você multiplicar um valor em centavos por um número decimal (como um imposto de 10%), o resultado pode ser fracionário. Nesse caso, você deve arredondar para o centavo mais próximo, usando Math.round().

function calcularImposto(valorCentavos, taxaPercentual) {
    // taxaPercentual é um número decimal, ex: 10 para 10%
    const imposto = valorCentavos * (taxaPercentual / 100);
    return Math.round(imposto); // arredonda para o centavo mais próximo
}

const preco = 1999;
const imposto = calcularImposto(preco, 10);
console.log(imposto); // 200 (R$ 2,00)
console.log(preco + imposto); // 2199 (R$ 21,99)

toFixed e armadilhas

O método toFixed() é frequentemente usado para formatar números com um número fixo de casas decimais. No entanto, ele tem algumas armadilhas importantes:

Primeiro, toFixed() retorna uma string, não um número. Isso pode causar problemas se você tentar fazer operações matemáticas diretamente com o resultado. Segundo, o arredondamento feito por toFixed() pode não ser o esperado em alguns casos, devido à representação binária. Por exemplo, (1.005).toFixed(2) retorna "1.00" em vez de "1.01", porque o número real armazenado é ligeiramente menor que 1.005.

console.log((1.005).toFixed(2)); // "1.00" (esperado "1.01")
console.log((0.1 + 0.2).toFixed(2)); // "0.30" (funciona, mas cuidado)
console.log(typeof (1.005).toFixed(2)); // string

Outra armadilha é que toFixed() arredonda para o número mais próximo, mas com regras específicas que podem variar entre navegadores (embora hoje em dia sigam o padrão). Para evitar surpresas, é melhor usar Math.round() com um fator de escala, ou trabalhar diretamente com centavos.

Se você precisar exibir um número com duas casas decimais, uma abordagem mais segura é converter para centavos, arredondar e depois formatar manualmente, como vimos na seção anterior. Alternativamente, use toLocaleString() com opções de moeda, que é mais robusto para formatação.

const valor = 1999; // centavos
const reais = valor / 100;
console.log(reais.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })); // R$ 19,99

Lembre-se: toLocaleString() também retorna uma string e formata de acordo com o locale, mas não resolve o problema de precisão em cálculos. Use-o apenas para exibição.

Boas práticas e observações finais

Ao trabalhar com dinheiro em JavaScript, adote estas boas práticas:

  • Sempre represente valores monetários como inteiros em centavos (ou a menor unidade da moeda).
  • Evite usar toFixed() para cálculos; use apenas para exibição.
  • Prefira Math.round() ao arredondar resultados de multiplicação/divisão.
  • Nunca compare números de ponto flutuante diretamente com ===; use uma margem de erro (Math.abs(a - b) < 1e-10) ou converta para inteiros.
  • Para aplicações financeiras críticas, considere usar bibliotecas especializadas como decimal.js ou big.js, que oferecem aritmética decimal exata.

Essas práticas ajudam a evitar erros sutis que podem custar caro em sistemas reais. Lembre-se de que a precisão é essencial quando lidamos com dinheiro.

Referências

Exercícios

  1. Explique por que 0.1 + 0.2 não é igual a 0.3 em JavaScript. Dê um exemplo de uma situação em que isso poderia causar um problema em um sistema financeiro.

    ✓ Resposta: JavaScript usa ponto flutuante de precisão dupla (IEEE 754), que não pode representar exatamente números como 0.1 em binário. Portanto, 0.1 e 0.2 são armazenados como aproximações, e a soma dessas aproximações resulta em 0.30000000000000004. Em um sistema financeiro, se você somar muitos valores com essas imprecisões, o total pode divergir do valor esperado, causando erros de arredondamento que podem levar a discrepâncias em relatórios ou transações.
  2. Converta os seguintes valores para centavos e realize a soma: R$ 12,34, R$ 0,99, R$ 100,00. Mostre o resultado em reais.

    ✓ Resposta:
    const valores = [1234, 99, 10000]; // em centavos
    const total = valores.reduce((acc, v) => acc + v, 0); // 11333
    const reais = Math.floor(total / 100);
    const centavos = total % 100;
    console.log(`R$ ${reais},${centavos.toString().padStart(2, '0')}`); // R$ 113,33
    
  3. Escreva uma função que receba um número em reais (ex: 19.99) e retorne o valor em centavos como inteiro. Em seguida, escreva uma função que faça o inverso (de centavos para reais formatado).

    ✓ Resposta:
    function reaisParaCentavos(valor) {
        return Math.round(valor * 100);
    }
    
    function centavosParaReais(centavos) {
        const reais = Math.floor(centavos / 100);
        const resto = centavos % 100;
        return `R$ ${reais},${resto.toString().padStart(2, '0')}`;
    }
    
    console.log(reaisParaCentavos(19.99)); // 1999
    console.log(centavosParaReais(1999)); // R$ 19,99
    
  4. Calcule o imposto de 15% sobre um produto que custa R$ 250,00, trabalhando com centavos. Arredonde o imposto para o centavo mais próximo. Mostre o preço final.

    ✓ Resposta:
    const precoCentavos = 25000; // R$ 250,00
    const imposto = Math.round(precoCentavos * 0.15); // 3750
    const total = precoCentavos + imposto; // 28750
    const reais = Math.floor(total / 100);
    const centavos = total % 100;
    console.log(`Preço final: R$ ${reais},${centavos.toString().padStart(2, '0')}`); // R$ 287,50
    
  5. Qual é o resultado de (1.005).toFixed(2)? Explique por que isso acontece e como você poderia obter o valor esperado (1.01) de forma segura.

    ✓ Resposta: O resultado é "1.00". Isso ocorre porque o número 1.005 é armazenado como uma aproximação binária ligeiramente menor que 1.005, então o arredondamento para duas casas decimais resulta em 1.00. Para obter 1.01, você pode usar uma estratégia de arredondamento com fator de escala: Math.round(1.005 * 100) / 100. Isso multiplica por 100, arredonda para o inteiro mais próximo e divide de volta, resultando em 1.01.