1

Valores grandes em APIs: preserve a precisão antes de chamar BigInt

Um painel pode apresentar um valor incorreto mesmo que a API tenha calculado tudo corretamente. Basta transformar um inteiro grande em Number antes de decidir como tratá-lo.

Isso merece atenção em integrações que recebem contadores, identificadores numéricos ou quantidades expressas em unidades mínimas. O problema não começa na formatação da tela: começa no contrato de dados.

Este exemplo é didático e independente de qualquer implementação de produção da Atlas. Vamos definir um formato restrito para quantidades não negativas e testar o caminho inteiro: entrada, soma, exibição e saída JSON.

O erro acontece antes da conversão

Execute em um ambiente JavaScript com suporte a BigInt:

const original = "9007199254740993";

console.log(Number(original).toString()); // "9007199254740992"
console.log(BigInt(Number(original)).toString()); // "9007199254740992"
console.log(BigInt(original).toString()); // "9007199254740993"

Converter para BigInt no fim não recupera o algarismo perdido. A documentação do Number.MAX_SAFE_INTEGER explica o limite de inteiros representáveis com segurança como Number.

A mesma armadilha aparece se o servidor enviar um literal numérico grande no JSON: quando a aplicação recebe o resultado do JSON.parse, a precisão pode já ter sido perdida. Para este contrato, a quantidade deve viajar como string decimal, desde a origem.

Um contrato pequeno e explícito

Vamos aceitar somente de 1 a 78 algarismos ASCII, sem sinal, espaços ou zeros à esquerda, exceto o próprio "0". O limite de 78 é uma escolha deste exemplo para limitar o tamanho da entrada: não equivale a validar o intervalo de um uint256.

O campo decimals é um inteiro de 0 a 18 que descreve a escala. A quantidade "1234500", com escala 4, será exibida como "123.4500".

function parseUnitsText(text) {
  if (typeof text !== "string" || text.length > 78) {
    throw new TypeError("Quantidade deve ser uma string de até 78 dígitos");
  }
  const match = /^(0|[1-9][0-9]*)$/.exec(text);
  if (!match || match[0] !== text) {
    throw new TypeError("Use um inteiro decimal não negativo e canônico");
  }
  return BigInt(text);
}

function formatUnits(units, decimals) {
  if (typeof units !== "bigint" || units < 0n) {
    throw new TypeError("Quantidade interna inválida");
  }
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 18) {
    throw new RangeError("Escala fora do intervalo de 0 a 18");
  }
  const digits = units.toString();
  if (decimals === 0) return digits;

  const padded = digits.padStart(decimals + 1, "0");
  return padded.slice(0, -decimals) + "." + padded.slice(-decimals);
}

A validação não tenta adivinhar a intenção de quem enviou "1,50", "1e3" ou " 10 ". Neste formato de transporte, esses valores são erros. Se a interface aceita vírgula decimal em português, ela precisa de uma etapa separada de entrada localizada, com regras claras; remover pontuação indiscriminadamente pode alterar o valor.

Teste as fronteiras, não só o exemplo bonito

Coloque o código acima em valores.mjs, acrescente os testes abaixo e execute node valores.mjs:

import assert from "node:assert/strict";

assert.equal(parseUnitsText("0"), 0n);
assert.equal(parseUnitsText("9007199254740993"), 9007199254740993n);
assert.equal(formatUnits(parseUnitsText("1234500"), 4), "123.4500");
assert.equal(formatUnits(1n, 6), "0.000001");
assert.equal(formatUnits(0n, 2), "0.00");
assert.equal(formatUnits(42n, 0), "42");

for (const invalid of ["", "01", "-1", "+1", "1.2", "1,2", "1e3", " 1", "1\n", "1", 1, null]) {
  assert.throws(() => parseUnitsText(invalid), TypeError);
}
assert.throws(() => parseUnitsText("9".repeat(79)), TypeError);
assert.throws(() => formatUnits(-1n, 2), TypeError);
assert.throws(() => formatUnits(1n, 1.5), RangeError);
assert.throws(() => formatUnits(1n, 19), RangeError);
assert.equal(parseUnitsText("9".repeat(78)).toString(), "9".repeat(78));
assert.equal(formatUnits(1n, 18), "0.000000000000000001");

const total = parseUnitsText("9007199254740993") + parseUnitsText("7");
const payload = JSON.stringify({ units: total.toString(), decimals: 6 });
const received = JSON.parse(payload);
assert.equal(parseUnitsText(received.units), total);
assert.equal(formatUnits(total, received.decimals), "9007199254.741000");
console.log("Testes concluídos.");

Os testes incluem um valor acima do limite seguro de Number, entradas ambíguas e a ida e volta pelo JSON. No objeto enviado, units volta a ser string: JSON.stringify não serializa BigInt diretamente por padrão. Esse comportamento e os cuidados nas conversões estão na documentação de BigInt.

O que esse exemplo não resolve

  • Unidades diferentes: não some valores só porque ambos são BigInt. Ativo, unidade e escala devem coincidir, ou existir uma conversão explícita.
  • Arredondamento: o formatador preserva todas as casas da escala. Ele não calcula taxas nem define regras de arredondamento.
  • Limites de negócio: uma string válida pode exceder o máximo permitido pela aplicação. Valide esse máximo também, inclusive depois de operações.
  • Origem confiável: preservar os dígitos não prova que o saldo, contador ou dado recebido seja verdadeiro ou atual.
  • Apresentação localizada: o ponto neste exemplo é deliberado. Não converta a string de volta para Number apenas para obter uma aparência mais bonita.

Para mim, a pergunta útil na revisão é: em que momento essa quantidade deixa de ser texto e qual contrato permite essa conversão? Se a primeira conversão já perde informação, nenhuma máscara visual vai corrigir o resultado.

Como vocês documentam unidade e escala nas APIs: em campos separados, no schema, ou no próprio nome do atributo?


Maria Torres — Atlas System, PR Team. Vínculo: Atlas System. Material educativo preparado com assistência de IA; os exemplos e testes foram executados localmente. Não descreve código de produção do projeto nem constitui recomendação de investimento.

Carregando publicação patrocinada...