2011-01-24 10 views
24

Estoy ejecutando pruebas NUnit para evaluar algunos datos de prueba conocidos y resultados calculados. Los números son dobles en coma flotante, así que no espero que sean exactamente iguales, pero no estoy seguro de cómo tratarlos con la misma precisión.Evaluar si dos dobles son iguales en función de una precisión determinada, no dentro de una cierta tolerancia fija

En NUnit podemos comparar con una tolerancia fija:

double expected = 0.389842845321551d; 
double actual = 0.38984284532155145d; // really comes from a data import 
Expect(actual, EqualTo(expected).Within(0.000000000000001)); 

y que funciona bien para los números bajo cero, pero a medida que los números crecen de la tolerancia que realmente necesita ser cambiado por lo que siempre se preocupan por el mismo número de dígitos de precisión.

En concreto, esta prueba falla:

double expected = 1.95346834136148d; 
double actual = 1.9534683413614817d; // really comes from a data import 
Expect(actual, EqualTo(expected).Within(0.000000000000001)); 

y por supuesto un mayor número fallan con la tolerancia ..

double expected = 1632.4587642911599d; 
double actual = 1632.4587642911633d; // really comes from a data import 
Expect(actual, EqualTo(expected).Within(0.000000000000001)); 

¿Cuál es la forma correcta de evaluar dos números de coma flotante son iguales con una precisión dada ? ¿Hay una forma incorporada de hacer esto en NUnit?

Respuesta

11

De MSDN:

Por defecto, un valor doble contiene 15 dígitos decimales de precisión, aunque un máximo de 17 dígitos se mantiene internamente.

Supongamos 15, entonces.

Entonces, podríamos decir que queremos que la tolerancia sea en la misma medida.

¿Cuántas cifras precisas tenemos después del punto decimal? Necesitamos saber la distancia del dígito más significativo del punto decimal, ¿verdad?La magnitud. Podemos obtener esto con un Log10.

Luego tenemos que dividir 1 por 10^precisión para obtener un valor alrededor de la precisión que queremos.

Ahora, usted tiene que hacer más casos de prueba que yo, pero esto parece funcionar:

double expected = 1632.4587642911599d; 
    double actual = 1632.4587642911633d; // really comes from a data import 

    // Log10(100) = 2, so to get the manitude we add 1. 
    int magnitude = 1 + (expected == 0.0 ? -1 : Convert.ToInt32(Math.Floor(Math.Log10(expected)))); 
    int precision = 15 - magnitude ; 

    double tolerance = 1.0/Math.Pow(10, precision); 

    Assert.That(expected, Is.EqualTo(actual).Within(tolerance)); 

Es tarde - que podría haber una Gotcha aquí. Lo probé contra tus tres conjuntos de datos de prueba y cada uno pasó. Si se cambia pricision por 16 - magnitude, la prueba falla. Establecerlo en 14 - magnitude obviamente hizo que pasara ya que la tolerancia era mayor.

+0

Esta es una muy buena solución, pero genera una excepción al comparar '0.0' a' 0.0'. Sin embargo, una simple condición 'si' alrededor de 'expected' se encargaría de eso. –

+0

Ah sí. Pensé que habría algo. :) He actualizado mi respuesta para tener esto en cuenta. – Brett

+0

esto funcionó muy bien. Ejecuté 105 pruebas unitarias, aproximadamente la mitad positivas y la mitad negativas, y todas menos 8 pasaron. La respuesta de Michael Borgwardt pasó y no pasó exactamente las mismas pruebas. –

5

¿Qué hay de convertir los artículos cada uno en cuerda y comparar las cuerdas?

string test1 = String.Format("{0:0.0##}", expected); 
string test2 = String.Format("{0:0.0##}", actual); 
Assert.AreEqual(test1, test2); 
+1

He pensado en esto y estoy de acuerdo en que funcionaría para convertir a cadenas. Sin embargo, como dijiste exactamente, te gustaría convertir a cadenas usando la precisión completa disponible para cada valor y luego truncarlas con la misma longitud. Sin embargo, estoy esperando un resultado matemáticamente apropiado, algo menos hacky. –

3

No sé si hay una forma integrada de hacerlo con nunit, pero sugeriría multiplicando cada flotador por el 10 veces la precisión que usted está buscando, almacenando los resultados que anhela, y comparando los dos anhelan el uno al otro.
Por ejemplo:

double expected = 1632.4587642911599d; 
double actual = 1632.4587642911633d; 
//for a precision of 4 
long lActual = (long) 10000 * actual; 
long lExpected = (long) 10000 * expected; 

if(lActual == lExpected) { // Do comparison 
    // Perform desired actions 
} 
+1

Esto es matemáticamente idéntico al uso de una tolerancia, simplemente se está aplicando de una manera diferente. –

+0

Pero esto es mucho más legible y más simple –

0

Ésta es una idea rápida, pero, ¿y desplazándolas hacia abajo hasta que están por debajo de cero? Debería ser algo como num/(10^ceil(log10(num))). . . no estoy seguro de qué tan bien funcionaría, pero es una idea.

1632.4587642911599/(10^ceil(log10(1632.4587642911599))) = 0.16324587642911599 
+0

Esto parece un buen comienzo al menos. ¿Qué efecto tiene este cálculo en la precisión? ¿hay una pérdida de precisión? Obviamente, no me importa la precisión total de ambos valores comparables, pero si perdemos precisión en ambos, la comparación puede dar algunos falsos positivos. –

8

Esto es lo que se me ocurrió para The Floating-Point Guide (código Java, sino que debe traducirse fácilmente, y viene con un conjunto de pruebas, lo que realmente realmente necesita):

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 == 0) { // a or b or both are zero 
     // relative error is not meaningful here 
     return diff < (epsilon * epsilon); 
    } else { // use relative error 
     return diff/(absA + absB) < epsilon; 
    } 
} 

La pregunta realmente complicado es qué hacer cuando uno de los números para comparar es cero. La mejor respuesta puede ser que tal comparación siempre debe considerar el significado del dominio de los números que se comparan en lugar de tratar de ser universales.

+0

@Michael Borgwardt, ¿qué es exactamente épsilon? ¿Qué significa un valor de 0.0001 en comparación con el hombre? A través de las pruebas puedo encontrar los valores apropiados para épsilon que funcionan para mis datos de prueba (ambos son verdaderos cuando se espera y falso cuando se espera), pero no estoy 100% claro en este parámetro. –

+2

@Samuel: epsilon es un margen de error relativo, es decir, épsilon de 0.01 significa que la diferencia entre los 2 valores debe ser inferior a aproximadamente 1%. Pero si un valor es 0, esto no tiene sentido, así que en ese caso requiero que el otro valor sea más pequeño que epsilon al cuadrado. Eso es bastante arbitrario; por lo tanto, la conclusión de que una función de comparación universalmente útil puede no existir. –

+0

@Michael Bordwardt, gracias, esa es una buena explicación. Comprendo y estoy de acuerdo con su afirmación acerca de una función de comparación universalmente útil, pero lamentablemente esto es para una biblioteca, no para una aplicación empresarial específica, entonces estoy buscando algo que sea lo más universal posible. Todavía estoy haciendo más pruebas, tanto su solución como Bretts (más simple, menos ordenada) parecen funcionar en los mismos casos y fallan en los mismos casos. –

0

¿Qué tal:

const double significantFigures = 10; 
Assert.AreEqual(Actual/Expected, 1.0, 1.0/Math.Pow(10, significantFigures)); 
+0

Por supuesto, si el valor esperado es cero o cercano, tendrá que codificar un caso especial. ¿Cuántos dígitos significativos tiene cero? Cero o infinito? – Pughjl

Cuestiones relacionadas