Comparaciones e igualdad: cuando 0.1 más 0.2 no es 0.3
Por qué un entero y un flotante pueden ser iguales sin conversión previa, cómo el redondeo binario destruye la igualdad ingenua, qué es NaN y por qué no es igual a sí mismo, y cómo comparar con tolerancia sin engañarte.
La comparación numérica parece la parte aburrida del nivel y es la que más código rompe. Lua hace aquí un trabajo silencioso y admirable: cuando comparas un entero de 64 bits con un flotante, no convierte uno al otro —eso perdería bits y daría respuestas falsas— sino que ejecuta una comparación matemáticamente exacta entre dos representaciones distintas. Y sin embargo, encima de esa base impecable, el punto flotante sigue haciendo lo que siempre hizo: dar respuestas correctas a preguntas que no eran las que creías hacer.
- Explicar por qué
1 == 1.0es verdadero y qué hace Lua por debajo para que sea seguro. - Localizar el origen binario de la desigualdad entre
0.1 + 0.2y0.3. - Comparar magnitudes con tolerancia absoluta y relativa, eligiendo cuál toca.
- Manejar los valores especiales: infinito,
NaNy su papel como clave de tabla.
Igualdad entre subtipos, hecha bien
En Lua, dos números son iguales si representan el mismo valor matemático, con independencia de su subtipo. La comparación es entre valores, no entre representaciones:
print(1 == 1.0) --> true
print(math.type(1) == math.type(1.0))--> false subtipos distintos
print(3 // 1 == 3.0) --> true
Lo interesante no es que sea verdadero, sino que sea fiable en los extremos. La implementación ingenua de esta comparación sería convertir el entero a flotante y comparar; ya viste en el nivel anterior que esa conversión redondea a partir de 53 bits, así que produciría igualdades falsas entre enteros distintos. Lua no hace eso: cuando compara un entero con un flotante, comprueba primero si el flotante tiene un valor entero exacto dentro del rango, y si no lo tiene, decide el orden analizando el flotante sin degradar el entero.
local grande = math.maxinteger -- 2^63 - 1
local flotante = 2.0^63 -- exactamente 2^63
print(grande == flotante) --> false correcto
print(grande < flotante) --> true correcto
print(grande + 0.0 == flotante) --> true aqui si perdiste bits
La diferencia entre la segunda línea y la cuarta es toda la lección: comparar directamente es exacto, convertir a mano y luego comparar no lo es. Si escribes una conversión explícita para “normalizar antes de comparar”, estás introduciendo el error que Lua se molestaba en evitar.
El orden relativo (<, <=) obedece las mismas reglas y también es exacto entre subtipos. La única excepción está en los valores especiales, que llegan al final.
Por qué 0.1 más 0.2 no da 0.3
Esto no es un defecto de Lua, es una consecuencia de escribir en base dos números que en base diez parecen redondos. Una fracción decimal es exactamente representable en binario solo si su denominador es una potencia de dos. Un décimo no lo es, así que 0.1 no existe como flotante: lo que existe es el flotante más cercano a un décimo.
print(0.1 + 0.2 == 0.3) --> false
print(string.format("%.17g", 0.1)) --> 0.10000000000000001
print(string.format("%.17g", 0.1 + 0.2)) --> 0.30000000000000004
print(string.format("%.17g", 0.3)) --> 0.29999999999999999
print(0.5 + 0.25 == 0.75) --> true potencias de dos
La última línea muestra que el problema no es “los flotantes son imprecisos” sino algo más específico: los flotantes son exactos para las fracciones diádicas y aproximados para el resto. Por eso el dinero, que es decimal por naturaleza, nunca debe representarse con flotantes: se guarda en céntimos, como entero, y se formatea al mostrarlo.
-- mal: acumula error y no cuadra la caja
local total = 0.0
for _ = 1, 10 do total = total + 0.1 end
print(total == 1.0) --> false
-- bien: enteros en la unidad minima, division solo al presentar
local centimos = 0
for _ = 1, 10 do centimos = centimos + 10 end
print(centimos == 100) --> true
print(string.format("%.2f", centimos / 100)) --> 1.00
Ojo con el otro extremo: tostring usa por defecto catorce dígitos significativos, así que imprimir un flotante puede ocultarte la diferencia que luego rompe la comparación. Cuando estés depurando algo numérico, formatea con %.17g, que es el número de dígitos que garantiza el viaje de ida y vuelta de un flotante de doble precisión.
Comparar con tolerancia, y hacerlo bien
Si no puedes usar la igualdad, la respuesta habitual es “compara con un épsilon”. La respuesta habitual está incompleta, porque hay dos tolerancias distintas y elegir la equivocada falla en direcciones opuestas.
-- tolerancia absoluta: valida solo cerca de cero
local function cerca_abs(a, b, eps)
return math.abs(a - b) <= (eps or 1e-9)
end
-- tolerancia relativa: escala con la magnitud
local function cerca_rel(a, b, eps)
eps = eps or 1e-9
local d = math.abs(a - b)
if d == 0 then return true end
return d <= eps * math.max(math.abs(a), math.abs(b))
end
-- combinada: lo que casi siempre quieres
local function cerca(a, b, eps)
eps = eps or 1e-9
return math.abs(a - b) <= eps * math.max(1.0, math.abs(a), math.abs(b))
end
La tolerancia absoluta falla con números grandes: para valores del orden de mil millones, una diferencia de una milmillonésima es indetectable y cerca_abs declarará distintos dos números que son el mismo hasta el último bit útil. La tolerancia relativa falla cerca de cero: si a y b valen ambos casi cero, el denominador se desvanece y la comparación se vuelve absurdamente estricta. La versión combinada, que toma el máximo entre uno y las magnitudes, se comporta como absoluta cerca del origen y como relativa lejos de él.
flowchart TD A[Necesito comparar dos flotantes] --> B[Los valores son magnitudes medidas o calculadas] B -->|no, son contadores o indices| C[Usa enteros y compara con igualdad exacta] B -->|si| D[Estan cerca de cero] D -->|si| E[Tolerancia absoluta] D -->|no| F[Tolerancia relativa] E --> G[En la duda usa la combinada con maximo de uno] F --> G style C fill:#a6e3a1,color:#11111b style G fill:#89b4fa,color:#11111b
La rama izquierda del diagrama es la más importante y la que menos se sigue: muchísimas comparaciones frágiles desaparecen si el valor nunca debió ser flotante. Un contador, un índice, un identificador y una cantidad de dinero son enteros; compararlos con == es correcto y no necesita tolerancia alguna.
Infinito, NaN y las claves de tabla
El punto flotante define tres valores que no son números en el sentido corriente, y los tres tienen reglas propias en las comparaciones.
print(math.huge) --> inf
print(1/0 == math.huge) --> true
print(-math.huge < math.mininteger) --> true
local nan = 0/0
print(nan == nan) --> false
print(nan ~= nan) --> true el unico test fiable
print(nan < 1, nan >= 1) --> false false
NaN es el único valor de Lua que no es igual a sí mismo, y esa anomalía es útil: x ~= x es la forma canónica y portátil de detectarlo, sin depender de tostring. Su otra propiedad, que toda comparación de orden con NaN sea falsa, tiene una consecuencia práctica desagradable en table.sort: una función de orden que reciba un NaN deja de ser un orden estricto y la ordenación puede lanzar el error de función de orden inválida, o peor, corromper la tabla. Filtra los NaN antes de ordenar, nunca después.
En las tablas hay una asimetría instructiva. Un flotante de valor entero se normaliza a clave entera —por eso t[1] y t[1.0] son la misma entrada—, pero un NaN no puede ser clave en absoluto:
local t = {}
t[2.0] = "dos"
print(t[2]) --> dos misma clave normalizada
t[math.huge] = "infinito" -- permitido: es un valor concreto
-- t[0/0] = "x" --> error: table index is NaN
La prohibición es coherente: una clave debe poder recuperarse comparándola consigo misma, y NaN no supera esa prueba. Lua prefiere el error explícito en la inserción a una entrada fantasma que nadie podría volver a leer jamás.
El operador == aplicado a flotantes hace exactamente lo que promete: comprueba si dos patrones de 64 bits denotan el mismo valor. Cuando ese resultado te sorprende, el defecto no está en el operador ni en el estándar IEEE 754, está en la pregunta: has preguntado si dos cantidades son idénticas cuando lo que querías saber es si son indistinguibles a efectos de tu dominio, y esas son dos preguntas distintas que ningún lenguaje puede unificar por ti, porque la segunda depende de información que solo tú tienes. La tolerancia correcta para comparar dos temperaturas de un sensor con dos décimas de resolución no tiene nada que ver con la tolerancia correcta para comparar dos coordenadas en un mapa a escala planetaria, ni con la de comparar dos saldos contables —donde, de hecho, la respuesta correcta es no usar flotantes en absoluto—. Por eso Lua no ofrece una función de comparación aproximada en su biblioteca estándar y hace bien en no ofrecerla: cualquier épsilon que eligiera sería el correcto para un dominio y silenciosamente destructivo para los demás. Lo que sí te da es una base honesta sobre la que construir la tuya: comparaciones exactas entre subtipos, un NaN que se delata a sí mismo, claves de tabla normalizadas y un math.type para saber siempre con qué estás tratando. La disciplina profesional consiste en escribir esa función de tolerancia una sola vez, documentarla con la unidad y la precisión del dominio al que sirve, y no volver a escribir == entre flotantes en el resto del programa.
- Encuentra el menor flotante positivo
epara el que1.0 + e ~= 1.0sea verdadero. Ese número tiene nombre propio: búscalo. - Suma 0.1 diez mil veces y compara el resultado con 1000.0 usando las tres funciones de tolerancia. Anota cuál acierta y por qué.
- Escribe
es_nan(x)sin usartostringy pruébala con0/0,math.huge - math.hugeymath.sqrt(-1). - Construye una tabla con claves 1, 1.0,
"1"ymath.huge, cuenta cuántas entradas tiene y explica el número. - Ordena con
table.sortuna lista que contenga unNaNen medio, observa qué ocurre y arregla el problema filtrando antes de ordenar.