learning-front

Nivel 3 · JavaScript moderno y asíncrono

Números: precisión, redondeo y BigInt

Por qué 0.1 + 0.2 no da 0.3 en ningún lenguaje, cómo evitar que ese error arruine un importe o una comparación, y qué es BigInt para los enteros de 64 bits que no caben en un number normal.

Sabes manejar datos, asincronía y errores. Antes de cerrar el nivel, hay una pieza de la librería estándar que aparece en cualquier proyecto con dinero, estadísticas o identificadores: la precisión numérica. Por qué 0.1 + 0.2 no da lo que esperas, qué hacer al respecto, y cuándo necesitas BigInt.

Seguimos con el Team Builder. Ahora trabajamos con los premios del torneo (importes con decimales) y los ids de jugador que llegan de la API. Dos problemas reales que se resuelven con las herramientas que verás aquí.

Por qué 0.1 + 0.2 no da 0.3#

Los números de JavaScript con decimales se guardan en formato binario con precisión limitada (64 bits, el estándar IEEE 754 que usan casi todos los lenguajes). El problema es que algunos valores decimales, como 0.1, no caben exactos en binario: solo existe una aproximación. Y cuando sumas dos aproximaciones, el error se arrastra:

javascript
// 0.30000000000000004, no 0.3
console.log(0.1 + 0.2);
// false: la diferencia es un resto de representación binaria
console.log(0.1 + 0.2 === 0.3);

No es un bug de JavaScript: pasa exactamente igual en Python, Java, C# y casi cualquier otro lenguaje que use el mismo estándar. La consecuencia práctica es que no puedes comparar decimales con === a pelo cuando esos decimales son el resultado de operaciones aritméticas.

Donde más muerde este problema es en el dinero: el total de un carrito, un importe con impuestos, el premio de un torneo. Un 0.30000000000000004 en la pantalla de pago es un error visible que destroza la confianza del usuario.

toFixed: mostrar con decimales fijos#

Cuando necesitas mostrar un número con un número fijo de decimales, toFixed(n) hace el trabajo. Ya lo viste en el nivel 2 al formatear el winrate; aquí lo aplicamos a importes:

javascript
const importe = 102.3;
// toFixed(2) devuelve un STRING con exactamente 2 decimales
// "102.30": hay un cero de relleno, y es un string, no un number
console.log(importe.toFixed(2));

Dos cosas importantes sobre toFixed:

  • Devuelve un string, no un number. Si necesitas seguir operando con el resultado, conviértelo con Number() o parseFloat(). Si solo vas a mostrarlo, el string ya está listo.
  • toFixed redondea al último decimal pedido. No elimina el error de representación del número original, pero sí lo tapa al convertirlo a texto con el número de decimales que controlas tú.
javascript
const totalCarrito = 0.1 + 0.2;
// El number interno es 0.30000000000000004
// Pero toFixed lo convierte a texto con 2 decimales: "0.30"
// "0.30": listo para mostrar en pantalla
console.log(totalCarrito.toFixed(2));

Comparar decimales con Number.EPSILON#

toFixed sirve para mostrar. Para comparar si dos decimales son “iguales” después de operar, necesitas otro enfoque. Number.EPSILON es la constante que JavaScript define como el margen mínimo entre dos values de tipo number que pueden ser distintos. Si la diferencia absoluta entre dos valores es menor que ese margen, se consideran iguales a efectos prácticos:

javascript
const a = 0.1 + 0.2;
const b = 0.3;

// Math.abs(x) devuelve el valor absoluto de x: siempre positivo.
// p. ej. Math.abs(-0.5) -> 0.5, Math.abs(0.5) -> 0.5
// Math.abs(a - b) es la diferencia en valor absoluto (positiva)
// Number.EPSILON es ~2.2e-16: el margen mínimo entre dos numbers distintos
// true: a y b son "iguales" a efectos prácticos
console.log(Math.abs(a - b) < Number.EPSILON);

Cuando el dominio es dinero, hay una solución más robusta aún: trabajar en céntimos (enteros) durante todos los cálculos y convertir a euros solo al mostrar. Los enteros no tienen error de representación. Lo verás en el ejercicio.

Métodos de Number#

Number tiene métodos para inspeccionar la naturaleza de un valor:

javascript
// Number.isInteger: ¿es un entero sin parte decimal?
// true
console.log(Number.isInteger(320));
// false: tiene parte decimal
console.log(Number.isInteger(0.5));
javascript
// Number.isNaN: ¿es NaN (resultado de operación sin sentido matemático)?
// 0 / 0 no tiene resultado válido: da NaN
// true
console.log(Number.isNaN(0 / 0));
// false: 42 es un número normal
console.log(Number.isNaN(42));

Ya viste Number.isNaN en el nivel 2, donde lo usamos para detectar conversiones fallidas (Number("abc") da NaN). Aquí lo retomamos para completar el cuadro de herramientas de Number.

BigInt: enteros más allá del límite seguro#

Un number normal tiene un límite: Number.MAX_SAFE_INTEGER, que es 9007199254740991 (aproximadamente 9 billones). Por encima de ese valor, dos enteros distintos pueden representarse con el mismo bit pattern y, en consecuencia, compararse como iguales aunque no lo sean:

javascript
// true (!): JavaScript los confunde porque superan el límite seguro
console.log(9007199254740993 === 9007199254740992);

Esto pasa con ids de 64 bits que devuelven APIs y bases de datos (PostgreSQL, MongoDB, Twitter, servicios de identidad). Si guardas esos ids en un number normal, puedes acabar apuntando al registro equivocado.

BigInt resuelve el problema: representa enteros de precisión ilimitada. Se escribe con una n al final del literal:

javascript
// BigInt con el literal: 9007199254740993n
// Los dos son distintos: BigInt no pierde ningún dígito
// false, correcto
console.log(9007199254740993n === 9007199254740992n);

Sin embargo, en el código que se ejecuta en Sandpack (el editor de esta página) el transpilador no entiende el sufijo n, así que usamos el constructor BigInt('...') pasándole el número como string. En un proyecto real o en la consola del navegador puedes usar el literal n sin problema; en el playground del curso, el constructor es la forma correcta:

javascript
// Cuando el id llega como string de una API o base de datos, usa el constructor.
// BigInt('9007199254740993') acepta el string directamente.
// false: BigInt distingue los dos valores
console.log(BigInt('9007199254740993') === BigInt('9007199254740992'));

BigInt es para enteros. No mezcla con number en operaciones aritméticas: 1n + 1 lanza un TypeError. Para mostrar un BigInt en un string, llama a .toString().

Pruébalo tú#

Mira la consola: el error de precisión, toFixed tapándolo, la comparación con EPSILON, los métodos de Number, y BigInt confirmando que dos ids distintos no colapsan. Cambia los valores y pulsa Ejecutar (o Ctrl+Enter) para observar los resultados.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Cuánto vale `0.1 + 0.2 === 0.3` en JavaScript?

Tu turno#

Tienes los premios del torneo (importes con decimales) y los ids de jugador (strings de 64 bits). Monta el informe mostrando cada importe formateado, el total correcto, y los ids manejados con BigInt. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente elimina el error en su raíz trabajando en céntimos durante toda la suma.

Ejercicio · en esta página

Cuadra las cuentas del equipo

Recibes los premios del torneo (importes con decimales) y los ids de jugador de 64 bits (que llegan como strings de la API). Monta un informe que muestre cada importe formateado a 2 decimales, sume el total sin error de precisión, y maneje los ids sin perder ningún dígito.

Paso 1: Que funcione

  • Muestras una fila por héroe con su nombre, rol e importe.
  • Sumas todos los importes y muestras el total.
  • Vale formatear el importe concatenando a mano y sumar con el operador + a pelo.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Suma los importes a pelo con el operador +, formatea pegando " €" a mano y
// compara el total con === a pelo. El informe se ve, pero...
//   - la suma arrastra el error de punto flotante: el total puede mostrar
//     decimales feos como "521.09999999999994" en vez de "521.10".
//   - formatear con toFixed existe, pero aquí se concatena a pelo: da el mismo
//     bug de decimales largos si no se redondea primero.
//   - comparar totales con === falla cuando la suma tiene error de precisión.
//   - los ids se muestran como strings, pero no se demuestra que BigInt los
//     maneja exactos: el bug de truncado queda invisible.
// Funciona en la pantalla si los decimales cuadran por suerte, pero no es fiable.
// ════════════════════════════════════════════════════════════════════════════

// copia local de los datos para que esta solución sea autocontenida
const PREMIOS = [
  { nombre: "Tracer", rol: "DPS", importe: 102.3 },
  { nombre: "Mercy", rol: "Soporte", importe: 98.7 },
  { nombre: "Reinhardt", rol: "Tanque", importe: 154.2 },
  { nombre: "Genji", rol: "DPS", importe: 89.1 },
  { nombre: "Ana", rol: "Soporte", importe: 76.8 },
];

const IDS_JUGADOR = [
  "9007199254740993",
  "9007199254740994",
  "9007199254740995",
];

// Suma todos los importes con el operador +.
// El error de precisión se acumula en cada suma.
let total = 0;
for (const p of PREMIOS) {
  // se acumula el error de punto flotante en cada iteración
  total = total + p.importe;
}

// Construye las líneas de texto de los premios. Formatea concatenando el símbolo a mano.
const lineasPremios = PREMIOS.map((p) => {
  // concatenar a pelo: si el número tiene muchos decimales, se ven todos
  const importeTexto = p.importe + "";
  return p.nombre + " | " + p.rol + " | " + importeTexto;
});

// La línea del total también concatena a pelo: puede mostrar decimales feos.
const lineaTotal = "Total: " + total + "";

// Muestra los ids tal cual llegan, sin BigInt.
// El bug de truncado queda invisible porque no se intenta comparar.
const lineasIds = IDS_JUGADOR.map((id) => {
  return id;
});

function mostrar(lineasPremios, lineasIds, lineaTotal) {
  console.log("=== Premios del torneo ===");
  for (const linea of lineasPremios) {
    console.log(linea);
  }
  // imprime el total al final de la sección de premios
  console.log(lineaTotal);
  console.log("=== Ids de jugador ===");
  for (const linea of lineasIds) {
    console.log(linea);
  }
}

mostrar(lineasPremios, lineasIds, lineaTotal);

Por qué este nivel

  • Suma los importes con el operador + a pelo: el error de punto flotante se acumula y el total puede mostrar '521.09999999999994' en vez de '521.10'.
  • Formatear concatenando el símbolo a mano no redondea: si el number tiene muchos decimales, se ven todos en la celda.
  • Los ids se muestran pero no se demuestra que BigInt los maneja sin truncar: el bug queda invisible.