<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vilson Neto</title>
    <description>The latest articles on DEV Community by Vilson Neto (@vilsonneto).</description>
    <link>https://dev.to/vilsonneto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1397915%2F1997dc92-93b1-4212-b4cd-f64278eceefd.jpeg</url>
      <title>DEV Community: Vilson Neto</title>
      <link>https://dev.to/vilsonneto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vilsonneto"/>
    <language>en</language>
    <item>
      <title>Por que a SEFAZ rejeita sua NF-e (e a culpa é do IEEE 754)</title>
      <dc:creator>Vilson Neto</dc:creator>
      <pubDate>Tue, 17 Mar 2026 09:47:11 +0000</pubDate>
      <link>https://dev.to/vilsonneto/por-que-a-sefaz-rejeita-sua-nf-e-e-a-culpa-e-do-ieee-754-13a</link>
      <guid>https://dev.to/vilsonneto/por-que-a-sefaz-rejeita-sua-nf-e-e-a-culpa-e-do-ieee-754-13a</guid>
      <description>&lt;p&gt;Abra o console do navegador e digite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;1.064&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;39680&lt;/span&gt;
&lt;span class="c1"&gt;// 42219.520000000004&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O resultado correto é 42219.52. O seu computador diz 42219.520000000004.&lt;/p&gt;

&lt;p&gt;Não é bug do JavaScript. É IEEE 754, o padrão de ponto flutuante que Python, C, Perl e JavaScript usam para armazenar decimais. É o mesmo motivo pelo qual &lt;code&gt;0.1 + 0.2 === 0.30000000000000004&lt;/code&gt;. Para a maioria das aplicações, a diferença não importa. Para emissão de NF-e no Brasil, importa. A SEFAZ compara o seu cálculo com o dela e rejeita a nota se divergir.&lt;/p&gt;

&lt;p&gt;Vale dizer: IEEE 754 não é um padrão ruim. Foi desenhado para computação científica, onde precisão relativa importa mais que representação decimal exata. Funciona bem para física, gráficos, machine learning. O problema é que sistemas fiscais precisam de aritmética decimal exata, e isso é fundamentalmente diferente do que IEEE 754 oferece.&lt;/p&gt;

&lt;p&gt;Neste artigo: onde o erro nasce, por que as soluções óbvias não funcionam e como eliminar o problema na raiz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Índice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rejeição 629: vProd ≠ vUnCom × qCom&lt;/li&gt;
&lt;li&gt;Rejeição 630: a regra que os ERPs esquecem&lt;/li&gt;
&lt;li&gt;O que não funciona&lt;/li&gt;
&lt;li&gt;Por que strings resolvem&lt;/li&gt;
&lt;li&gt;Resumo&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Rejeição 629: vProd ≠ vUnCom × qCom
&lt;/h2&gt;

&lt;p&gt;A regra 629 verifica se o valor total do produto (&lt;code&gt;vProd&lt;/code&gt;) é igual ao preço unitário (&lt;code&gt;vUnCom&lt;/code&gt;) multiplicado pela quantidade (&lt;code&gt;qCom&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;O erro não está no cálculo. Está na atribuição. Quando você escreve &lt;code&gt;const preco = 1.064&lt;/code&gt;, o valor que entra na memória já não é 1.064.&lt;/p&gt;

&lt;p&gt;IEEE 754 usa 64 bits para representar um número decimal: 1 de sinal, 11 de expoente e 52 de mantissa. Muitos decimais não têm representação exata em base 2. O que acontece com 1.064:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frghsnr3i0nn8ftex6zl7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frghsnr3i0nn8ftex6zl7.png" alt="Representação do número 1.064 em ponto flutuante IEEE 754 mostrando sinal, expoente e mantissa, evidenciando a aproximação binária do valor decimal." width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preco&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.064&lt;/span&gt;     &lt;span class="c1"&gt;// armazenado como ~1.0640000000000001&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qtd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;39680&lt;/span&gt;        &lt;span class="c1"&gt;// inteiro, representação exata&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;preco&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;qtd&lt;/span&gt; &lt;span class="c1"&gt;// 42219.520000000004&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nesse caso específico, &lt;code&gt;toFixed(2)&lt;/code&gt; ainda produz &lt;code&gt;'42219.52'&lt;/code&gt; porque o drift é pequeno demais pra afetar o arredondamento. Mas nem sempre. Dois cenários reais onde o drift causa rejeição:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2ww3ttuuiqar7ryo5oe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2ww3ttuuiqar7ryo5oe.png" alt="Exemplos práticos de erro de arredondamento em JavaScript com cálculos de preço por litro e divisão de embalagem, mostrando divergência de R$ 0,01 após uso de toFixed(2)." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combustível:&lt;/strong&gt; posto vende gasolina a R$ 5,799/L. Cliente abastece 15 litros.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;5.799&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="c1"&gt;// 86.985 ← aparenta estar certo&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;5.799&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '86.98' ← errado, HALF_UP daria '86.99'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O valor armazenado é &lt;code&gt;86.984999999999999&lt;/code&gt;. O 5 da terceira casa virou 4. R$ 0,01 de diferença. Rejeição 629.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribuidora:&lt;/strong&gt; compra pack de 6 refrigerantes a R$ 6,99. Vende 9 unidades avulsas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vUnCom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;6.99&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;   &lt;span class="c1"&gt;// 1.165&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vUnCom&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// '10.48' ← errado, HALF_UP daria '10.49'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O vUnCom é calculado por divisão, como todo ERP faz. O drift acumula na multiplicação: &lt;code&gt;10.484999999999999&lt;/code&gt;. Mais R$ 0,01. Mais uma rejeição.&lt;/p&gt;

&lt;p&gt;O mesmo padrão se repete em grãos, farmacêutico, autopeças, material de construção, químico, têxtil. Qualquer mercado onde o preço unitário é calculado por divisão de embalagem e o resultado cai na fronteira de arredondamento.&lt;/p&gt;

&lt;p&gt;A SEFAZ não compara só o total da nota. Ela recalcula &lt;code&gt;vUnCom × qCom&lt;/code&gt; para cada item individualmente. Se um item diverge, mesmo que a soma total bata, a nota é rejeitada.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rejeição 630: a regra que os ERPs esquecem
&lt;/h2&gt;

&lt;p&gt;A maioria dos ERPs testa a 629 e ignora a 630. A 630 valida os campos de tributação, que frequentemente têm precisão diferente dos campos comerciais.&lt;/p&gt;

&lt;p&gt;A NT 2023.004 define a precisão de cada campo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Campo&lt;/th&gt;
&lt;th&gt;Descrição&lt;/th&gt;
&lt;th&gt;Casas decimais&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vUnCom&lt;/td&gt;
&lt;td&gt;Valor unitário de comercialização&lt;/td&gt;
&lt;td&gt;até 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qCom&lt;/td&gt;
&lt;td&gt;Quantidade comercial&lt;/td&gt;
&lt;td&gt;até 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vUnTrib&lt;/td&gt;
&lt;td&gt;Valor unitário de tributação&lt;/td&gt;
&lt;td&gt;até 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qTrib&lt;/td&gt;
&lt;td&gt;Quantidade tributária&lt;/td&gt;
&lt;td&gt;até 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vProd&lt;/td&gt;
&lt;td&gt;Valor total do produto&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Quando você multiplica um número com 10 casas por um com 4, o resultado intermediário tem até &lt;strong&gt;14 casas decimais&lt;/strong&gt; antes do arredondamento para 2. É nesse intervalo que IEEE 754 falha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vUnTrib com 10 casas, qTrib com 4 casas&lt;/span&gt;
&lt;span class="mf"&gt;0.0000000012&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1000.1234&lt;/span&gt;
&lt;span class="c1"&gt;// 0.0000012001480799999999&lt;/span&gt;
&lt;span class="c1"&gt;// resultado exato: 0.00000120014808&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A divergência aparece quando a NF-e usa unidades de tributação diferentes da comercialização: litros vs caixas, kg vs unidades. Os campos mudam, a precisão muda, e o drift que passava despercebido na 629 estoura na 630.&lt;/p&gt;

&lt;p&gt;Muitos ERPs nem testam a 630 em homologação porque usam a mesma unidade para comercialização e tributação nos testes. Quando o cliente real manda uma nota com unidades diferentes, a rejeição aparece em produção.&lt;/p&gt;




&lt;h2&gt;
  
  
  O que não funciona
&lt;/h2&gt;

&lt;p&gt;Antes de chegar na solução, vale entender por que as abordagens óbvias falham.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;toFixed&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;O primeiro instinto de todo dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.064&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;39680&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// '42219.52' ← funciona neste caso&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funciona por sorte. &lt;code&gt;toFixed&lt;/code&gt; arredonda com base no valor em memória, não no valor que você digitou. O número 1.255 é armazenado como 1.2549999... Quando &lt;code&gt;toFixed&lt;/code&gt; vê um valor abaixo de .5, arredonda para baixo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.255&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '1.25' ← deveria ser '1.26' (HALF_UP)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '2.67' ← deveria ser '2.68'&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.005&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '1.00' ← deveria ser '1.01'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O arredondamento padrão da SEFAZ é HALF_UP: dígito &amp;gt;= 5, arredonda para cima. Não é banker's rounding (HALF_EVEN), que distribui arredondamentos estatisticamente. É o arredondamento que você aprendeu na escola. A maioria dos sistemas fiscais do mundo usa HALF_UP.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toFixed&lt;/code&gt; não garante HALF_UP porque opera sobre a representação IEEE 754 que já chegou corrompida. O problema não é o algoritmo de arredondamento, é o input que alimenta esse algoritmo.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Math.round&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Dois problemas. Primeiro, &lt;code&gt;Math.round&lt;/code&gt; arredonda "toward +Infinity", não HALF_UP. Para positivos funciona igual, mas para negativos diverge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// -0  (toward +Infinity)&lt;/span&gt;
&lt;span class="c1"&gt;// HALF_UP seria  // -1  (away from zero)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segundo, e mais importante: &lt;code&gt;Math.round&lt;/code&gt; recebe o mesmo float corrompido. O drift já aconteceu antes da chamada. Nenhuma função de arredondamento resolve se o valor que ela recebe já não é o que você digitou.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiplicar por 100 (centavos)
&lt;/h3&gt;

&lt;p&gt;A abordagem clássica: trabalhar em centavos para evitar decimais. Com um preço de 2 casas como R$ 10.64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;precoEmCentavos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1064&lt;/span&gt;  &lt;span class="c1"&gt;// R$ 10.64&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qtd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;39680&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;precoEmCentavos&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;qtd&lt;/span&gt; &lt;span class="c1"&gt;// 42219520&lt;/span&gt;
&lt;span class="c1"&gt;// Dividir por 100 → R$ 422195.20 ✓&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funciona para valores com 2 casas decimais. O problema é que a NF-e não trabalha com 2 casas. O campo &lt;code&gt;vUnCom&lt;/code&gt; aceita até 10 casas decimais, e o &lt;code&gt;qCom&lt;/code&gt; até 4. Para representar tudo como inteiro, você precisaria multiplicar por 10^10 e 10^4 respectivamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vUnCom: 1.0640000000 × 10^10 → 10640000000&lt;/span&gt;
&lt;span class="c1"&gt;// qCom:   39680.0000   × 10^4  → 396800000&lt;/span&gt;
&lt;span class="c1"&gt;// Produto: 10640000000 * 396800000 = 4.22 × 10^18&lt;/span&gt;
&lt;span class="c1"&gt;// Number.MAX_SAFE_INTEGER        = 9.007 × 10^15&lt;/span&gt;
&lt;span class="c1"&gt;// Excedeu. Drift de volta.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BigInt
&lt;/h3&gt;

&lt;p&gt;Resolve o overflow, mas não tem divisão decimal nativa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10640000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;396800000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// 4221952000000000000n ← correto&lt;/span&gt;

&lt;span class="c1"&gt;// Mas: quanto é isso em reais?&lt;/span&gt;
&lt;span class="c1"&gt;// Precisa dividir por 10^14 e arredondar com HALF_UP&lt;/span&gt;
&lt;span class="c1"&gt;// BigInt não tem .round() nem divisão com resto decimal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Você acabaria reimplementando aritmética decimal por cima do BigInt. Nesse ponto, é mais simples operar diretamente sobre strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparativo
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abordagem&lt;/th&gt;
&lt;th&gt;Resolve o drift?&lt;/th&gt;
&lt;th&gt;Garante HALF_UP?&lt;/th&gt;
&lt;th&gt;Funciona com 10+ casas?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toFixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Math.round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;td&gt;Não&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Centavos (×100)&lt;/td&gt;
&lt;td&gt;Sim, até 2 casas&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Não (overflow)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigInt&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Sim, mas sem divisão decimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strings&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;td&gt;Sim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Por que strings resolvem
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2j6ubpqbcn980sjnf4uh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2j6ubpqbcn980sjnf4uh.png" alt="Ilustração da representação de números decimais como strings, destacando estrutura com sinal, dígitos e expoente, e como isso evita erros de precisão do IEEE 754." width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Se o número já entra corrompido como float, a solução é não deixar entrar como float.&lt;/p&gt;

&lt;p&gt;Quando você passa &lt;code&gt;'1.064'&lt;/code&gt; como string, cada dígito é preservado. Não existe conversão para binário. Não existe truncamento de mantissa. Internamente, a representação é:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// { sign: 1 | -1, digits: string, exponent: number }&lt;/span&gt;
&lt;span class="c1"&gt;// '1.064' → { sign: 1, digits: "1064", exponent: -3 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A multiplicação opera sobre esses dígitos, como você faria no papel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Multiplica os dígitos inteiros: &lt;code&gt;1064 × 39680 = 42219520&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Soma os expoentes: &lt;code&gt;-3 + 0 = -3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Aplica o expoente: &lt;code&gt;42219520 × 10^-3 = 42219.520&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Resultado exato. Nenhum bit é perdido porque nenhum bit foi usado para representar o número. É texto puro do começo ao fim.&lt;/p&gt;

&lt;p&gt;A mesma lógica funciona para qualquer precisão. Dez casas decimais? Sem problema, são só dígitos numa string. Não existe &lt;code&gt;MAX_SAFE_INTEGER&lt;/code&gt; para strings. Não existe truncamento de mantissa.&lt;/p&gt;

&lt;p&gt;Essa não é uma ideia nova. Bancos de dados fazem isso há décadas. O tipo &lt;code&gt;NUMERIC&lt;/code&gt;/&lt;code&gt;DECIMAL&lt;/code&gt; do PostgreSQL armazena dígitos, não floats. O Java tem &lt;code&gt;BigDecimal&lt;/code&gt;. Python tem o módulo &lt;code&gt;decimal&lt;/code&gt;. A diferença é que no JavaScript não existe tipo decimal nativo. A &lt;a href="https://github.com/tc39/proposal-decimal" rel="noopener noreferrer"&gt;proposta TC39 Decimal&lt;/a&gt; existe, mas ainda está em andamento. Então a solução, por enquanto, é usar uma lib que implemente isso.&lt;/p&gt;

&lt;p&gt;Existem libs que fazem isso: &lt;code&gt;decimal.js&lt;/code&gt;, &lt;code&gt;big.js&lt;/code&gt;, &lt;code&gt;bignumber.js&lt;/code&gt;. Todas resolvem a precisão. A diferença está no que vem junto. Para o contexto fiscal brasileiro, construí a &lt;a href="https://github.com/vilsonneto/tributos-br" rel="noopener noreferrer"&gt;tributos-br&lt;/a&gt;, que adiciona arredondamento HALF_UP por padrão (não precisa configurar), sete modos de arredondamento e calculadoras fiscais integradas (ICMS, IPI, DIFAL, ST):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tributos-br/precision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preco&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.064&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quantidade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;39680&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;preco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantidade&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// '42219.52' ← exato, sempre&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sem conversão para IEEE 754 em nenhum momento. E os três casos que &lt;code&gt;toFixed&lt;/code&gt; erra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.255&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '1.26' ← HALF_UP correto&lt;/span&gt;
&lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.675&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '2.68' ← HALF_UP correto&lt;/span&gt;
&lt;span class="nx"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.005&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '1.01' ← HALF_UP correto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O resultado não depende de sorte. É exato por construção.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resumo
&lt;/h2&gt;

&lt;p&gt;Três coisas pra levar deste artigo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;O erro nasce na atribuição, não no cálculo.&lt;/strong&gt; &lt;code&gt;const preco = 1.064&lt;/code&gt; já perdeu precisão antes de qualquer operação.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;toFixed&lt;/code&gt;, &lt;code&gt;Math.round&lt;/code&gt; e centavos não são soluções.&lt;/strong&gt; Operam sobre o float corrompido, ou esbarram nos limites de precisão da NF-e.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Aritmética em strings elimina o problema na raiz.&lt;/strong&gt; Sem conversão para binário, sem drift, sem surpresas.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Se você emite NF-e e quer testar: &lt;a href="https://www.npmjs.com/package/tributos-br" rel="noopener noreferrer"&gt;&lt;code&gt;npm install tributos-br&lt;/code&gt;&lt;/a&gt;. Zero dependências, TypeScript strict, &lt;a href="https://github.com/vilsonneto/tributos-br" rel="noopener noreferrer"&gt;código aberto&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Esse é o primeiro post da série &lt;em&gt;Precisão Numérica em TypeScript&lt;/em&gt;. No próximo, vou cobrir DIFAL e o problema da base dupla vs base única. Se quiser acompanhar, segue o perfil aqui no DEV.&lt;/p&gt;

&lt;p&gt;Você já enfrentou problemas de arredondamento em produção? Em qual contexto? Conta nos comentários.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artigo teve assistência de IA na revisão e estruturação. Todo código e dados técnicos foram verificados manualmente.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
