Precisión de coma flotante: por qué 0.1 + 0.2 ≠ 0.3
0.1 + 0.2 devuelve 0.30000000000000004 porque ni 0.1 ni 0.2 existen dentro de un número binario de coma flotante. Un double solo puede guardar valores de la forma m × 2ⁿ, y un décimo es una fracción periódica infinita en base 2, de modo que el hardware se queda con el vecino representable más cercano. El double que hay detrás de 0.1 es exactamente 0.1000000000000000055511151231257827021181583404541015625. El que hay detrás de 0.2 es 0.200000000000000011102230246251565404236316680908203125. Suma esos dos valores almacenados y la suma exacta cae entre dos doubles representables. IEEE 754 redondea al más cercano, que queda un pelo por encima de 0.3.
Esa es toda la respuesta. La precisión de coma flotante es finita, las fracciones decimales rara vez encajan en la rejilla binaria y cada operación vuelve a redondear.
Esto no es una rareza de JavaScript ni un defecto de la CPU. El mismo resultado aparece en Python, Java, C, Go, Rust, Swift y en las columnas FLOAT de SQL, porque todos corren sobre IEEE 754. Pega cualquier valor en el convertidor IEEE 754 de coma flotante y verás los bits y el valor almacenado completo, dígito por dígito.
Falta el detalle: de dónde sale exactamente el error, por qué tu lenguaje a veces lo esconde, cómo comparar flotantes sin == y qué tipo numérico elegir cuando la precisión cuesta dinero.
La precisión de coma flotante en 60 segundos
Tres hechos explican casi todas las sorpresas:
- La coma flotante binaria solo puede representar números de la forma m × 2ⁿ, es decir, una suma de potencias de dos.
- Las fracciones decimales como
0.1,0.2y0.3no tienen esa forma, y se redondean al entrar. - Cada operación aritmética vuelve a redondear su resultado al valor representable más cercano.
| Decimal | ¿Representable de forma exacta? | Por qué |
|---|---|---|
| 0.5 | Sí | 2⁻¹ |
| 0.25 | Sí | 2⁻² |
| 0.75 | Sí | 2⁻¹ + 2⁻² |
| 2.5 | Sí | 2 + 2⁻¹ |
| 100 | Sí | Cualquier entero por debajo de 2⁵³ |
| 0.1 | No | 1/10 — el denominador tiene un factor 5 |
| 0.2 | No | 1/5 — el mismo problema |
| 0.3 | No | 3/10 — el mismo problema |
Regla práctica: simplifica la fracción; si el denominador queda como una potencia pura de dos, el valor es exacto. Cualquier factor 5 que sobreviva significa que solo se guarda una aproximación.
Por qué 0.1 no se puede almacenar de forma exacta
Los bits que siguen a la coma binaria llevan los pesos 1/2, 1/4, 1/8, 1/16, 1/32, y así sucesivamente. Intenta armar 0.1 con ellos y nunca caes justo encima. Obtienes 1/16 = 0.0625, más 1/32 = 0.09375, más 1/256 = 0.09765625, cada vez más cerca, nunca exacto. En binario, un décimo es 0.0001100110011… con 0011 repitiéndose para siempre.
El sistema decimal tiene el mismo problema con otras fracciones. Escribir 1/3 en base 10 da 0.333… y ninguna cadena finita de dígitos lo alcanza. A nadie se le ocurre llamar a eso un bug del decimal. La base 2 simplemente traza la línea en otro lugar, y a 1/10 le toca caer del lado equivocado. La documentación de Python, Aritmética de coma flotante: problemas y limitaciones, recorre la misma expansión si quieres la versión larga.
El binario no es más que la base 2, y la misma aritmética posicional gobierna el hexadecimal y el octal. El conversor binario, hex, decimal y octal muestra cómo se mueve un valor entre bases, y nuestra guía de conversión de bases para desarrolladores cubre a fondo el lado de los enteros. Las fracciones son donde la base 2 empieza a doler.
Qué guarda realmente un double
Un double de 64 bits se divide en tres campos: 1 bit de signo, 11 bits de exponente y 52 bits de mantisa. La mantisa lleva un 1 inicial implícito que nunca se almacena, con lo que dispones de 53 bits de significando, algo así como 15.95 dígitos decimales de precisión de coma flotante. El exponente se guarda con un sesgo de 1023.
Pídele a cualquier lenguaje más dígitos de los que imprime normalmente y la aproximación aparece:
>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20); // '0.10000000000000000555'
(0.1).toPrecision(20); // '0.10000000000000000555'
Decimal(0.1) es la vista honesta: convierte el double que ya tienes, sin volver a redondear, e imprime los 55 dígitos. El panel de precisión del convertidor IEEE 754 de coma flotante hace lo mismo con cualquier número que escribas, y añade el error de redondeo con signo respecto a lo que ingresaste.
La aritmética exacta detrás de 0.30000000000000004
Ese resultado concreto no es un fallo cercano cualquiera: sale de un empate exacto y de la regla que IEEE 754 usa para romperlo.
Suma exactamente esos dos valores almacenados, sin ningún redondeo, y obtienes:
0.1 → 0.1000000000000000055511151231257827021181583404541015625
0.2 → 0.200000000000000011102230246251565404236316680908203125
sum → 0.3000000000000000166533453693773481063544750213623046875
Esa suma no es en sí misma un double representable. Queda entre dos vecinos de la rejilla binaria:
below: 0.299999999999999988897769753748434595763683319091796875 (prints as 0.3)
above: 0.3000000000000000444089209850062616169452667236328125 (prints as 0.30000000000000004)
Mide ahora las distancias. La suma exacta está 2⁻⁵⁵ ≈ 2.776e-17 por encima del vecino inferior y 2⁻⁵⁵ por debajo del superior: no está más cerca de ninguno de los dos, sino exactamente a mitad de camino. Un empate.
El redondeo al más cercano se queda sin distancia con la que trabajar, de modo que IEEE 754 aplica su regla de desempate: redondeo al par (round-half-to-even), o sea, elegir el candidato cuyo último bit de mantisa sea 0. El significando del vecino inferior es 5404319552844595, que es impar. El del superior es 5404319552844596, que es par. Gana el valor par, lo que da el patrón de bits 0x3FD3333333333334: un ULP por encima del double al que apunta el literal 0.3, e impreso como 0.30000000000000004.
¿Por qué preferir el par? Redondear siempre los empates hacia arriba sesgaría las sumas largas en una sola dirección. Alternar hacia el vecino par mantiene esa deriva cerca de cero a lo largo de muchas operaciones. Es el mismo redondeo bancario que usan los contadores, estandarizado en silicio, y es la razón directa de que el error de redondeo de coma flotante más famoso de internet termine en ...04.
Compáralo con una suma que no necesita ningún redondeo:
0.5 + 0.25 === 0.75; // true
0.1 + 0.2 === 0.3; // false
0.5, 0.25 y 0.75 son 2⁻¹, 2⁻² y 2⁻¹ + 2⁻². Los tres son exactos, su suma es exacta y la igualdad se comporta como sugiere la aritmética de la escuela. El panel de vecinos del convertidor muestra los valores representables anterior y siguiente alrededor de lo que ingreses, junto con la separación en ULP, para que puedas revisar la rejilla alrededor de 0.3 por tu cuenta.
Por qué tu lenguaje a veces esconde el error
Esto es lo que hace que todo el asunto parezca embrujado: 0.1 se imprime como 0.1, pero 0.1 + 0.2 imprime diecisiete dígitos. El mismo formato de almacenamiento, salidas radicalmente distintas.
Los runtimes modernos usan formato de ida y vuelta más corto (shortest round-trip): emiten la cadena decimal más corta que se vuelve a parsear al double idéntico. "0.1" ya lleva sin ambigüedad al valor guardado para 0.1, y eso es lo que ves. La suma 0.1 + 0.2 es un double distinto del que corresponde a "0.3", por lo que el formateador tiene que seguir agregando dígitos hasta que la cadena sea inequívoca, y para eso hacen falta los diecisiete. El trabajo de redondeo correcto de David Gay en 1990 inició este linaje; Grisu y después Ryu lo volvieron lo bastante rápido para cualquier biblioteca estándar. Tu lenguaje no te miente. Te muestra la etiqueta más corta que identifica ese valor y ningún otro.
Comportamiento lenguaje por lenguaje
| Lenguaje | Tipo flotante por defecto | 0.1 + 0.2 imprime | Opción decimal exacta |
|---|---|---|---|
| JavaScript | number (solo double) | 0.30000000000000004 | Ninguna incorporada: centavos enteros o una biblioteca |
| Python | float (double) | 0.30000000000000004 | decimal.Decimal, fractions.Fraction |
| Java | double | 0.30000000000000004 | BigDecimal (constrúyelo desde una cadena) |
| C# | double | 0.30000000000000004 | decimal — 128 bits, base 10 |
| Go | float64 | 0.30000000000000004 (con variables) | math/big.Rat |
| Rust | f64 | 0.30000000000000004 | El crate rust_decimal |
| SQL | FLOAT / DOUBLE PRECISION | 0.30000000000000004 | NUMERIC / DECIMAL |
Dos de esas filas esconden una trampa.
Go pliega las constantes con precisión arbitraria. Una expresión literal se evalúa exactamente en tiempo de compilación y solo después se convierte, con lo que la constante 0.1 + 0.2 se vuelve exactamente 0.3 antes siquiera de llegar a ser un float64:
package main
import "fmt"
func main() {
fmt.Println(0.1 + 0.2) // 0.3 — constant folded exactly, then converted
a, b := 0.1, 0.2
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(a+b == 0.3) // false
}
El BigDecimal de Java hereda el error si le pasas un double. El constructor convierte fielmente los bits que recibe:
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3
En SQL, la diferencia la marca el tipo de columna más que el dialecto:
-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3; -- t
-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8; -- f
-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2; -- 0.3
SELECT 0.1 + 0.2 = 0.3; -- 0 (false)
Detente un momento en ese par de SQLite. El resultado impreso dice 0.3, la comparación dice lo contrario, y los dos tienen razón.
Comparar flotantes: por qué == falla y Number.EPSILON no es una tolerancia
0.1 + 0.2 === 0.3 es falso porque el lado izquierdo es un double distinto. El consejo habitual es comparar con una tolerancia, y la versión más repetida de ese consejo está mal:
Math.abs(a - b) < Number.EPSILON; // ⚠️ not a general-purpose comparison
Number.EPSILON vale 2.220446049250313e-16, que es 2⁻⁵². MDN lo define como la diferencia entre 1 y el double más pequeño mayor que 1. Léelo otra vez: es la separación de la rejilla en 1.0, no un presupuesto de error universal. La separación entre flotantes se duplica en cada potencia de dos, de ahí que un umbral constante esté mal en todas partes salvo en el vecindario donde se midió.
Cerca de 1.0 da la casualidad de que funciona:
Math.abs((0.1 + 0.2) - 0.3); // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true
Escala el mismo cálculo por mil millones y se derrumba:
const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;
a === b; // false
Math.abs(a - b); // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles
Alrededor de 1e9 la separación entre doubles vecinos ronda 1.19e-7. Como 1e9 cae en [2²⁹, 2³⁰), el ULP ahí es 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7, aproximadamente 500 millones de veces mayor que Number.EPSILON. Cualquier error de redondeo real de esa magnitud aplasta al umbral, y la comprobación devuelve falso siempre. Allá abajo, en 1e-20, esa misma constante peca de generosa y declara iguales valores que obviamente son distintos.
Tolerancia absoluta, relativa y en ULP
Tres herramientas, tres trabajos:
- La tolerancia absoluta,
|a − b| <= atol, es la correcta cuando conoces la magnitud de antemano, y es lo único que funciona contra cero, porque una tolerancia relativa alrededor de 0 siempre vale 0. - La tolerancia relativa,
|a − b| <= rtol × max(|a|, |b|), escala con los datos a lo largo de las magnitudes, pero degenera cerca de cero. - La distancia en ULP cuenta cuántos valores representables caben entre los dos patrones de bits. Es la más precisa y la menos legible.
Combina las dos primeras y obtienes la forma que usa cualquier biblioteca numérica seria:
function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
if (a === b) return true; // handles Infinity === Infinity
const diff = Math.abs(a - b);
return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}
nearlyEqual(0.1 + 0.2, 0.3); // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9); // true
nearlyEqual(0, 1e-15); // true
nearlyEqual(1, 1.0001); // false
Python trae esto en la biblioteca estándar, y el allclose de NumPy usa la misma fórmula:
import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12) # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9) # True
Para la versión estricta, mapea los bits a un ordenamiento entero monótono y resta:
const view = new DataView(new ArrayBuffer(8));
function toOrdinal(x) {
view.setFloat64(0, x);
const i = view.getBigInt64(0);
return i >= 0n ? i : -(1n << 63n) - i; // make negatives order correctly
}
function ulpDistance(a, b) {
const d = toOrdinal(a) - toOrdinal(b);
return d < 0n ? -d : d;
}
ulpDistance(0.1 + 0.2, 0.3); // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9); // 1n
ulpDistance(-0, 0); // 0n
Por cierto, el == exacto no siempre está mal. Sirve para doubles de valor entero por debajo de 2⁵³, para constantes que asignaste y sobre las que nunca calculaste, y para comprobar si un valor es exactamente cero.
Cuando la precisión cuesta dinero: elegir el tipo numérico correcto
El dinero y los flotantes binarios son una mala pareja, y no porque un error suelto sea grande. Los errores son sistemáticos, reproducibles e invisibles hasta que un auditor los encuentra:
19.99 * 100; // 1998.9999999999998
Math.round(19.99 * 100); // 1999
(1.005).toFixed(2); // '1.00' — 1.005 is stored as 1.00499999999999989...
El segundo caso atrapa equipos todos los años. toFixed no redondeó mal. El valor que recibió ya estaba por debajo de 1.005.
Unidades menores enteras
Guarda centavos, no dólares. 1999 significa $19.99, la aritmética corre sobre enteros y solo divides en el momento de mostrar el resultado.
const priceCents = 1999; // $19.99
const subtotal = priceCents * 3; // 5997 — exact
const totalCents = 1000; // $10.00 split three ways
const share = Math.floor(totalCents / 3); // 333
const remainder = totalCents - share * 3; // 1 cent to allocate
Dos advertencias. Por encima de Number.MAX_SAFE_INTEGER (9007199254740991, o 2⁵³ − 1) los enteros de JavaScript dejan de ser exactos, así que usa BigInt. Y los enteros no deciden tu política de redondeo: las divisiones, los porcentajes y los repartos de impuestos siguen necesitando una regla explícita sobre adónde va el centavo sobrante.
Tipos decimales
Un tipo decimal almacena dígitos en base 10, y ahí las fracciones decimales entran sin redondeo. Constrúyelo desde una cadena, jamás desde un flotante, o heredarás el error binario antes de empezar:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
Decimal(0.1) # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m); // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m); // True
Java tiene BigDecimal, C# un decimal nativo de 128 bits, SQL un NUMERIC(12,2). Todos arreglan las fracciones decimales. Ninguno arregla 1/3, porque un tipo decimal sigue siendo un formato de coma flotante; solo movió la base del 2 al 10.
Matriz de decisión
| Caso de uso | Tipo recomendado | Por qué |
|---|---|---|
| Dinero, facturación, impuestos | Unidades menores enteras o decimal | Aritmética decimal exacta, redondeo auditable |
| Ciencia, física | double (FP64) | 15–16 dígitos sobran; el mejor soporte de bibliotecas |
| Geometría, gráficos | float o double + tolerancia | Ya es aproximado; compara con epsilon |
| Entrenamiento de ML | bfloat16 | El rango de FP32 con la mitad de memoria |
| Inferencia de ML, almacenamiento | FP16 | Más bits de mantisa cuando los valores están normalizados |
| Contadores, IDs, claves | Entero de 64 bits o BigInt | Nunca pertenecieron a un flotante |
Acumulación de errores y cancelación catastrófica
Un error de redondeo mide 1e-17 y es inofensivo. Diez mil de ellos son un ticket de soporte:
let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;
total; // 1000.0000000001588
total - 1000; // 1.588205122970976e-10
La deriva crece más o menos con el número de operaciones. En un bucle de este tamaño es invisible; en una agregación nocturna sobre millones de filas, no.
Cancelación catastrófica
El fallo más desagradable es la resta. Toma dos números casi iguales, réstalos y sus dígitos iniciales coincidentes se cancelan; queda un resultado dominado por el error que ya se había acumulado. El error absoluto no crece. El error relativo explota, porque el resultado ahora es minúsculo mientras que el error conservó su tamaño.
La fórmula de varianza de una pasada de los libros de texto, E[x²] − (E[x])², se mete de lleno en ese agujero:
const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;
// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean; // 0
// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length; // 0.6666666666666666
La varianza poblacional verdadera es 2/3. La fórmula de una pasada devuelve exactamente cero: no un poco desviado, sino una respuesta completamente distinta, porque ambos operandos rondaban 1e18 y la diferencia entre ellos vive por debajo del último bit almacenado. El algoritmo de Welford esquiva esto: actualiza la media y la suma de desviaciones al cuadrado dato a dato, sin llegar a formar nunca esa resta.
La fórmula cuadrática sufre lo mismo cuando b² ≫ 4ac, y se arregla igual:
const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);
(-b + disc) / (2 * a); // -7.450580596923828e-9 — about 25% wrong
(2 * c) / (-b - disc); // -1e-8 — correct
Ambas líneas calculan la misma raíz. La primera resta dos números casi iguales; la segunda reordena el álgebra para no tener que hacerlo nunca. El texto de Goldberg What Every Computer Scientist Should Know About Floating-Point Arithmetic sigue siendo el tratamiento de referencia sobre cancelación y cotas de error.
Sumación de Kahan
La sumación compensada de Kahan lleva un registro continuo de los bits de orden bajo que cada suma va descartando, y luego los reinyecta en la siguiente iteración:
function kahanSum(values) {
let sum = 0;
let compensation = 0;
for (const value of values) {
const y = value - compensation;
const t = sum + y;
compensation = (t - sum) - y; // the bits that fell off
sum = t;
}
return sum;
}
kahanSum(new Array(10000).fill(0.1)); // 1000 — exactly
Python te regala esto. math.fsum([0.1] * 10_000) devuelve 1000.0, y desde CPython 3.12 la función incorporada sum() también usa compensación de Neumaier, así que sum([0.1] * 10_000) es exacto mientras que un bucle explícito con += sigue derivando hasta 1000.0000000001588.
Recurre a la compensación en sumas largas, agregados financieros e integración numérica. Sáltatela con un puñado de valores o cuando ya pasaste a un tipo exacto.
Los valores especiales que rompen suposiciones
IEEE 754 reserva patrones de bits para valores que no obedecen las reglas que esperas:
NaN === NaN; // false — mandated by the standard
Number.isNaN(NaN); // true
[NaN].indexOf(NaN); // -1 (uses ===)
[NaN].includes(NaN); // true (uses SameValueZero)
-0 === 0; // true
Object.is(-0, 0); // false
1 / -0; // -Infinity
1e308 * 10; // Infinity — overflow never throws
Infinity - Infinity; // NaN
Number.MIN_VALUE; // 5e-324 — the smallest subnormal double
NaN compara como distinto de todo, incluso de sí mismo, y es a propósito: así un cálculo fallido nunca puede hacerse pasar por un resultado válido. Compruébalo con Number.isNaN() o con math.isnan() de Python.
El desbordamiento es más silencioso y más peligroso: devuelve ±Infinity y sigue adelante, propagándose aguas abajo hasta que alguien nota un gráfico lleno de huecos. El cero negativo es un patrón de bits distinto que aun así resulta igual a +0 bajo ===; aparece por el subdesbordamiento de un valor negativo o a partir de -1 * 0, y solo la división o Object.is lo delatan.
Los subnormales llenan el hueco entre el cero y el número normal más pequeño. Con el campo de exponente todo en ceros, el 1 inicial implícito se descarta y la mantisa se encoge gradualmente hacia cero, de modo que la diferencia entre dos flotantes distintos nunca se redondea a cero exacto. El precio es una precisión que se va perdiendo y, en cierto hardware, un desplome brusco de rendimiento. Los botones de valores especiales del convertidor IEEE 754 de coma flotante cargan ±0, ±Infinity, NaN y el subnormal más pequeño para que inspecciones cada patrón de bits directamente.
float vs double vs FP16 vs bfloat16
El mismo estándar, distintos presupuestos de rango y de precisión de coma flotante:
| Formato | Bits totales | Exponente | Mantisa | Dígitos decimales aprox. | Máximo finito | Uso típico |
|---|---|---|---|---|---|---|
| binary16 (FP16) | 16 | 5 | 10 | ~3.3 | 65504 | Inferencia en GPU, almacenamiento compacto |
| bfloat16 | 16 | 8 | 7 | ~2.4 | ~3.39 × 10³⁸ | Entrenamiento de ML |
| binary32 (float) | 32 | 8 | 23 | ~7.2 | ~3.40 × 10³⁸ | Gráficos, sensores, GPU |
| binary64 (double) | 64 | 11 | 52 | ~15.9 | ~1.80 × 10³⁰⁸ | El predeterminado en todo lo demás |
Los bits de exponente compran rango; los bits de mantisa compran precisión. FP16 gasta su presupuesto en precisión y se topa con un techo de 65504, lo bastante cerca de los valores reales de gradiente como para que el desbordamiento a Infinity sea un riesgo rutinario. bfloat16 hace el intercambio opuesto: conserva los ocho bits de exponente de FP32 y se conforma con siete bits de mantisa, por lo que los valores que desbordarían FP16 pasan sin problema y el entrenamiento rara vez necesita escalado de pérdida.
Usa double por defecto. Baja a un formato más estrecho solo después de haber medido un cuello de botella de memoria o de ancho de banda, y comprueba cuánto cuesta ese estrechamiento ingresando el mismo valor en cada formato del convertidor.
Reglas prácticas para la precisión de coma flotante
- Nunca uses
==con flotantes calculados. Compara con una tolerancia combinada, absoluta más relativa, dimensionada según tus datos. - No trates
Number.EPSILONcomo un umbral: describe la rejilla en 1.0 y nada más. - Mantén el dinero en unidades menores enteras o en un tipo decimal, y construye siempre los decimales desde cadenas.
- Compensa las sumas largas con Kahan, Neumaier o
math.fsum. - Reordena las fórmulas que restan cantidades casi iguales en lugar de ajustar tolerancias a su alrededor.
- Formatea a mano lo que van a leer las personas, con
toFixed, un f-string oprintf. Nunca le entregues unreprpor defecto a los usuarios. - Envía los números entre servicios como cadenas o enteros. Una ida y vuelta por JSON a través de un double es silenciosa y con pérdida.
- Elige
NUMERIC, noFLOAT, para las columnas de moneda. Arreglar esto después del lanzamiento significa una migración y una reconciliación. - Piensa en bits cuando un valor parezca imposible. Ese hábito es el que sostiene nuestra guía de operaciones bit a bit: AND, OR, XOR y máscaras, y convierte los misterios de la coma flotante en aritmética que puedes verificar.
FAQ
¿Están rotas las matemáticas de coma flotante?
Las matemáticas de coma flotante no están rotas: siguen IEEE 754 al pie de la letra. El estándar representa números reales con una cantidad finita de dígitos binarios, y las fracciones decimales como 0.1 no tienen forma binaria exacta, por eso se guarda el valor representable más cercano. Lo que está roto es la expectativa de que todo decimal encaje.
¿Por qué Python imprime 0.1 como 0.1 pero 0.1 + 0.2 como 0.30000000000000004?
El repr de Python usa formato de ida y vuelta más corto: imprime la cadena decimal más corta que se vuelve a parsear al double idéntico. "0.1" ya identifica su double y ningún otro. La suma 0.1 + 0.2 es un double distinto del que corresponde a "0.3", de ahí que el formateador tenga que emitir los diecisiete dígitos para no quedar ambiguo.
¿Por qué no debería usar Number.EPSILON para comparar dos flotantes?
Number.EPSILON (unos 2.22e-16) es la distancia entre 1 y el siguiente double, no un presupuesto de error universal. La separación entre flotantes se duplica en cada potencia de dos, así que cerca de 1e9 los doubles vecinos quedan a unos 1.19e-7 de distancia. Cualquier error de redondeo genuino ahí supera a EPSILON, con lo que la comparación resulta siempre falsa. Usa una tolerancia relativa o combinada.
¿Puedo usar coma flotante para dinero si redondeo al final?
Redondear al final no rescata al dinero en coma flotante. Los errores se acumulan a lo largo de sumas, multiplicaciones y repartos de varios pasos, y el momento en que redondeas cambia el total. 19.99 * 100 ya da 1998.9999999999998. Una auditoría exige aritmética exacta y reproducible: centavos enteros, decimal, BigDecimal o el NUMERIC de SQL.
¿Qué números decimales se pueden almacenar de forma exacta en un double?
Solo los valores expresables como m / 2ⁿ con m y n enteros dentro de la precisión del formato. Eso cubre 0.5, 0.25, 0.125, 0.75 y 2.5, más todos los enteros hasta 2⁵³. Simplifica la fracción primero: cualquier factor 5 que quede en el denominador, como en 0.1, 0.2, 0.3 o 0.7, significa que el valor se aproxima.
¿Por qué 0.1 + 0.2 === 0.3 es falso pero 0.5 + 0.25 === 0.75 es verdadero?
Porque 0.5, 0.25 y 0.75 son 2⁻¹, 2⁻² y 2⁻¹ + 2⁻²: los tres son exactamente representables y su suma no necesita redondeo. De 0.1, 0.2 y 0.3 solo se guarda una aproximación, y redondear la suma de los dos primeros cae un ULP por encima del double al que apunta 0.3.
¿Ocurre esto en todos los lenguajes de programación?
Todo lenguaje construido sobre coma flotante binaria IEEE 754 muestra el mismo comportamiento: JavaScript, Python, Java, C, C++, Go, Rust, Swift y el FLOAT de SQL. Lo que cambia es la precisión de impresión por defecto y si viene de fábrica un tipo decimal exacto, como el decimal de C# o el módulo decimal de Python.