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:
// 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:
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()oparseFloat(). Si solo vas a mostrarlo, el string ya está listo. toFixedredondea 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ú.
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:
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:
// Number.isInteger: ¿es un entero sin parte decimal?
// true
console.log(Number.isInteger(320));
// false: tiene parte decimal
console.log(Number.isInteger(0.5));// 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:
// 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:
// 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:
// 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.
Paso 2: Que esté pulido
- Usas toFixed(2) para formatear cada importe.
- Usas Math.abs(a - b) < Number.EPSILON para comparar el total con un valor esperado.
- Usas BigInt('…') para los ids de jugador y muestras que se representan exactos.
Paso 3: Que sea excelente
- Trabajas en céntimos (enteros) durante toda la suma para eliminar el error en su raíz.
- Funciones puras de conversión y formateo separadas de la presentación.
- La solución es reutilizable: cambiar los importes no requiere tocar la lógica de suma.
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.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - toFixed(2) formatea cada importe a exactamente 2 decimales: "102.30 €"
// en vez del número a pelo (que puede traer decimales extras).
// - Math.abs(a - b) < Number.EPSILON compara el total con un valor esperado
// sin romper cuando la suma arrastró un error de punto flotante mínimo.
// - BigInt('…') crea los enteros de 64 bits desde el string de la API:
// se puede comparar y operar sin perder ningún dígito.
//
// Su límite respecto a Excelente: la SUMA sigue haciéndose en decimales, así
// que el total puede seguir teniendo el error de precisión antes de toFixed.
// toFixed lo tapa al mostrarlo, pero el cálculo interno no es exacto.
// ════════════════════════════════════════════════════════════════════════════
// 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. El error de precisión puede estar presente,
// pero toFixed lo neutraliza al convertir a string para mostrar.
var total = 0;
for (var i = 0; i < PREMIOS.length; i++) {
// sigue siendo aritmética de punto flotante: el error se acumula
total = total + PREMIOS[i].importe;
}
// Construye las líneas de texto de los premios.
const lineasPremios = PREMIOS.map((p) => {
// toFixed(2) devuelve un STRING con exactamente 2 decimales: "102.30"
// No es lo mismo que el number: es la representación visual
const importeTexto = p.importe.toFixed(2) + " €";
return p.nombre + " | " + p.rol + " | " + importeTexto;
});
const totalEsperado = 521.1;
// Math.abs obtiene el valor absoluto de la diferencia (siempre positivo)
// Number.EPSILON es el margen mínimo entre dos numbers distintos en JS (~2.2e-16)
const sonIguales = Math.abs(total - totalEsperado) < Number.EPSILON;
// "521.10 €" (o muy cercano: el error decimal queda oculto)
const lineaTotal =
"Total" +
(sonIguales ? " (cuadrado)" : " (diferencia de redondeo)") +
": " +
total.toFixed(2) +
" €";
// BigInt('…') crea el entero exacto desde el string de la API.
// Comparar con === a otros BigInt es seguro: no pierden ningún dígito.
const lineasIds = IDS_JUGADOR.map((id) => {
// creamos el BigInt desde el string, como llega del backend
const bigId = BigInt(id);
return id + " (BigInt: " + bigId.toString() + ")";
});
function mostrar(lineasPremios, lineasIds, lineaTotal) {
console.log("=== Premios del torneo ===");
for (var i = 0; i < lineasPremios.length; i++) {
console.log(lineasPremios[i]);
}
// imprime el total al final de la sección de premios
console.log(lineaTotal);
console.log("=== Ids de jugador ===");
for (var j = 0; j < lineasIds.length; j++) {
console.log(lineasIds[j]);
}
}
mostrar(lineasPremios, lineasIds, lineaTotal); Por qué es mejor que el anterior
- toFixed(2) formatea cada importe a exactamente 2 decimales: '102.30 €'. No opera con el string; solo lo usa para mostrar.
- Math.abs(total - esperado) < Number.EPSILON compara los totales con tolerancia: no falla aunque la suma arrastre un error mínimo.
- Su límite: la SUMA sigue haciéndose en decimales, así que el total interno puede tener el error antes de que toFixed lo tape. Para cuentas de dinero en producción, trabajar en céntimos es más robusto.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Trabaja en CÉNTIMOS (enteros) para sumar sin error de punto flotante.
// Multiplicar por 100 al recibir y dividir por 100 al mostrar elimina
// el problema en su raíz: los enteros no tienen error de precisión.
// - La conversión céntimos -> euros usa Math.round para asegurar que la
// vuelta al decimal es exacta aunque haya un resto de representación.
// - Funciones puras (importeACentimos, centimosAEuros, formatearImporte,
// procesarId) separan la lógica de la presentación: cada una se prueba
// aparte y el informe solo recibe strings ya montados.
// - BigInt('…') sigue siendo el patrón para los ids: correcto, sin cambios.
// ════════════════════════════════════════════════════════════════════════════
// 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",
];
// Convierte un importe en euros a céntimos (entero).
// Math.round elimina el resto de representación binaria al pasar a entero.
// 102.3 -> 10230
function importeACentimos(importe) {
return Math.round(importe * 100);
}
// Convierte céntimos de vuelta a euros, con exactamente 2 decimales.
// División entre 100 y toFixed: el resultado es el string visual limpio.
// 52110 -> "521.10"
function centimosAEuros(centimos) {
return (centimos / 100).toFixed(2);
}
// Función pura: dado un id de jugador como string, devuelve el texto de la línea.
// Crea el BigInt para confirmar que el entero de 64 bits se maneja exacto.
function procesarId(id) {
// BigInt desde el string: preserva todos los dígitos sin truncar
const bigId = BigInt(id);
return id + " (verificado como BigInt: " + bigId.toString() + ")";
}
// Suma total en céntimos: enteros, sin error de punto flotante.
var totalCentimos = 0;
for (var i = 0; i < PREMIOS.length; i++) {
// importeACentimos convierte antes de sumar: la suma ocurre en enteros
totalCentimos = totalCentimos + importeACentimos(PREMIOS[i].importe);
}
// Construye las líneas de texto de los premios.
const lineasPremios = PREMIOS.map((p) => {
return (
p.nombre +
" | " +
p.rol +
" | " +
centimosAEuros(importeACentimos(p.importe)) +
" €"
);
});
// La línea del total usa la suma en céntimos: exacta por naturaleza.
const lineaTotal = "Total: " + centimosAEuros(totalCentimos) + " €";
// Lista de ids con verificación BigInt.
const lineasIds = IDS_JUGADOR.map((id) => {
return procesarId(id);
});
function mostrar(lineasPremios, lineasIds, lineaTotal) {
console.log("=== Premios del torneo ===");
for (var i = 0; i < lineasPremios.length; i++) {
console.log(lineasPremios[i]);
}
// imprime el total al final de la sección de premios
console.log(lineaTotal);
console.log("=== Ids de jugador ===");
for (var j = 0; j < lineasIds.length; j++) {
console.log(lineasIds[j]);
}
}
mostrar(lineasPremios, lineasIds, lineaTotal); Por qué es mejor que el anterior
- Trabaja en CÉNTIMOS (enteros) durante toda la suma: los enteros no tienen error de punto flotante. Solo se convierte a decimal al mostrar.
- Funciones puras (importeACentimos, centimosAEuros, procesarId) separan la lógica de la presentación: cada una se puede probar aparte.
- BigInt('…') sigue siendo el patrón para los ids: el string de la API se convierte directamente, sin riesgo de truncado.