Comparación
Debido a los errores de redondeo, la mayoría de los números de punto flotante terminan siendo ligeramente imprecisos. Mientras esta imprecisión se mantenga pequeña, normalmente se puede ignorar. Sin embargo, esto significa también que números que se espera que sean iguales (por ejemplo al calcular el mismo resultado utilizando distintos métodos correctos) a veces difieren levemente, y una simple prueba de igualdad falla. Por ejemplo:
float a = 0.15 + 0.15
float b = 0.1 + 0.2
if(a == b) // ¡Puede ser falso!
if(a >= b) // ¡También puede ser falso!
No usar márgenes de error absolutos
La solución es comprobar no si los números son exactamente iguales, sino si su diferencia es muy pequeña. El margen de error frente al que se compara esta diferencia normalmente se llama epsilon. En su forma más simple:
if(Math.abs(a-b) < 0.00001) // Mal ─ no hacer esto
Esto es una mala forma de hacerlo porque un epsilon fijo elegido porque «parece pequeño» podría perfectamente ser demasiado grande cuando los números que se comparan son también muy pequeños. La comparación devolvería «verdadero» para números muy diferentes. Y cuando los números son muy grandes, el epsilon puede acabar siendo más pequeño que el mínimo error de redondeo, por lo que la comparación siempre devolvería «falso». Por tanto, es necesario ver si el error relativo es menor que epsilon:
if(Math.abs((a-b)/b) < 0.00001) // ¡Todavía no es correcto!
Vigila los casos límite
Hay algunos casos especiales importantes para los que esto falla:
- Cuando tanto
a
comob
son cero.0.0/0.0
es NaN, lo que provoca una excepción en algunas plataformas o devuelve falso para todas las comparaciones. - Cuando solo
b
es cero, la división devuelve infinito, lo que también puede causar una excepción, o es mayor que epsilon incluso cuandoa
es más pequeño. - Devuelve «falso» cuando tanto
a
comob
son muy pequeños pero a ambos lados del cero, incluso cuando son los números no nulos más pequeños.
Además, el resultado no es conmutativo (nearlyEquals(a,b)
no es siempre lo mismo que
nearlyEquals(b,a)
). Para solucionar estos problemas, el código tiene que ser mucho más
complejo, así que necesitamos meterlo en una función:
public static boolean nearlyEqual(float a, float b, float epsilon)
{
final float absA = Math.abs(a);
final float absB = Math.abs(b);
final float diff = Math.abs(a - b);
if (a == b) { // Atajo, maneja los infinitos
return true;
} else if (a * b == 0) { // a o b o ambos son cero
// El error relativo no es importante aquí
return diff < (epsilon * epsilon);
} else { // Usar el error relativo
return diff / (absA + absB) < epsilon;
}
}
Este método pasa las pruebas para muchos casos especiales
importantes, pero como puedes ver, utiliza cierta lógica no trivial. En particular,
tiene que utilizar una definición totalmente distinta del margen de error cuando
a
o b
son cero, porque la definición clásica del error relativo es inútil en
esos casos.
Hay algunos casos en los que el método de arriba todavía produce resultados inesperados (concretamente, es mucho más estricto cuando un valor es casi cero que cuando es exactamente cero), y algunas de esas pruebas para las que fue desarrollado probablemente especifica un comportamiento que no es apropiado para algunas aplicaciones. Antes de usarlo, ¡asegúrate de que es adecuado para tu aplicación!
Comparando valores de punto flotante como enteros
Hay una alternativa a aplicar toda esta complejidad conceptual a una tarea aparentemente
tan sencilla: en lugar de comparar a
y b
como números
reales, podemos concebirlos como pasos
discretos y definir el margen de error como el número máximo de valores de punto flotante
posibles entre esos dos números.
Esto es conceptualmente muy evidente y fácil y tiene la ventaja de que escala implícitamente el margen de error relativo con la magnitud de los valores. Técnicamente es un poco más complejo, pero no mucho más de lo que puedas pensar, porque los números de punto flotante del IEEE 754 están diseñados para mantener su orden cuando sus secuencias de bits se interpretan como enteros.
Sin embargo, este método requiere que el lenguaje de programación soporte conversión entre valores de punto flotante y secuencias de bits enteras. Lee el artículo en inglés Comparing floating-point numbers para más detalles.
© Publicado en http://puntoflotante.org/ bajo una licencia Creative Commons Atribución Unported (BY). Original en inglés por Michael Borgwardt en http://floating-point-gui.de/.