Problemas de doble precisión en .NET

Tengo una función simple de C #:

public static double Floor(double value, double step) { return Math.Floor(value / step) * step; } 

Eso calcula el número más alto, más bajo o igual que “valor”, que es múltiplo de “paso”. Pero carece de precisión, como se ve en las siguientes pruebas:

 [TestMethod()] public void FloorTest() { int decimals = 6; double value = 5F; double step = 2F; double expected = 4F; double actual = Class.Floor(value, step); Assert.AreEqual(expected, actual); value = -11.5F; step = 1.1F; expected = -12.1F; actual = Class.Floor(value, step); Assert.AreEqual(Math.Round(expected, decimals),Math.Round(actual, decimals)); Assert.AreEqual(expected, actual); } 

El primero y el segundo afirmativo están bien, pero el tercero falla, porque el resultado solo es igual hasta el sexto lugar decimal. ¿Porqué es eso? ¿Hay alguna manera de corregir esto?

Actualización Si depuro la prueba, veo que los valores son iguales hasta el octavo lugar decimal en lugar del sexto, tal vez porque Math.Round introduce cierta imprecisión.

Nota En mi código de prueba escribí el sufijo “F” (constante de flotación explícita) donde quise decir “D” (doble), así que si cambio eso puedo tener más precisión.

Si omite todas las postfixes F (es decir, -12.1 vez de -12.1F ) obtendrá igualdad con algunos dígitos más. Sus constantes (y especialmente los valores esperados) ahora son flotantes debido a la F Si lo haces a propósito, explícalo.

Pero, por lo demás, estoy de acuerdo con las otras respuestas al comparar los valores dobles o float para la igualdad, simplemente no es confiable.

La aritmética de punto flotante en las computadoras no es Exact Science :).

Si desea precisión exacta para un número predefinido de decimales, use Decimal en lugar de doble o acepte un intervalo menor.

De hecho, me gustaría que no hubieran implementado el operador == para flotantes y dobles. Casi siempre es algo incorrecto preguntar si alguna vez un doble o un flotador es igual a cualquier otro valor.

http://en.wikipedia.org/wiki/Floating_point#Accuracy_problems

Por ejemplo, la no representabilidad de 0.1 y 0.01 (en binario) significa que el resultado de intentar cuadrar 0.1 no es ni 0.01 ni el número representable más cercano a él.

Use solo punto flotante si desea la interpretación (binaria) de sistemas numéricos de una máquina. No puedes representar 10 centavos.

Si quieres precisión, usa System.Decimal. Si desea velocidad, use System.Double (o System.Float). Los números de coma flotante no son números de “precisión infinita” y, por lo tanto, afirmar la igualdad debe incluir una tolerancia. Siempre que sus números tengan una cantidad razonable de dígitos significativos, esto está bien.

  • Si está buscando hacer cálculos matemáticos en números muy grandes Y muy pequeños, no use float o double.
  • Si necesita una precisión infinita, no use float o double.
  • Si está agregando una gran cantidad de valores, no use float o double (los errores se combinarán).
  • Si necesita velocidad y tamaño, use flotante o doble.

Vea esta respuesta (también mía) para un análisis detallado de cómo la precisión afecta el resultado de sus operaciones matemáticas.

Verifique las respuestas a esta pregunta: ¿es seguro comprobar los valores de punto flotante para la igualdad en 0?

Realmente, solo revisa “dentro de la tolerancia de …”

los flotadores y los dobles no pueden almacenar con precisión todos los números. Esta es una limitación con el sistema de coma flotante IEEE. Para tener una precisión fiel necesita usar una biblioteca matemática más avanzada.

Si no necesita precisión más allá de cierto punto, quizás el decimal funcione mejor para usted. Tiene una precisión más alta que el doble.

Para el problema similar, termino usando la siguiente implementación que parece tener éxito en la mayoría de mi caso de prueba (hasta 5 dígitos de precisión):

 public static double roundValue(double rawValue, double valueTick) { if (valueTick <= 0.0) return 0.0; Decimal val = new Decimal(rawValue); Decimal step = new Decimal(valueTick); Decimal modulo = Decimal.Round(Decimal.Divide(val,step)); return Decimal.ToDouble(Decimal.Multiply(modulo, step)); } 

A veces, el resultado es más preciso de lo que cabría esperar de strict: FP IEEE 754. Eso es porque HW usa más bits para el cálculo. Ver la especificación C # y este artículo

Java tiene la palabra clave strictfp y C ++ tiene modificadores de comstackdor. Echo de menos esa opción en .NET