c# unit-testing double double-precision epsilon

c# - ¿Por qué agregar double.epsilon a un valor da como resultado el mismo valor, perfectamente igual?



unit-testing double-precision (4)

En C99 y C ++, la función que hace lo que intentabas hacer se llama nextafter y está en math.h No sé si C # tiene algún equivalente, pero si lo tiene, esperaría que tuviera un nombre similar.

Tengo una prueba de unidad, probando límites:

[TestMethod] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void CreateExtent_InvalidTop_ShouldThrowArgumentOutOfRangeException() { var invalidTop = 90.0 + Double.Epsilon; new Extent(invalidTop, 0.0, 0.0, 0.0); } public static readonly double MAX_LAT = 90.0; public Extent(double top, double right, double bottom, double left) { if (top > GeoConstants.MAX_LAT) throw new ArgumentOutOfRangeException("top"); // not hit }

Pensé que solo iba a inclinar el 90.0 sobre el borde agregando el mínimo posible doble positivo, pero ahora no se lanza la excepción, ¿alguna idea de por qué?

Al realizar la depuración, veo que la parte superior aparece como 90, cuando debería ser 90.00000000 ... algo.

EDITAR: Debería haber pensado un poco más, 90+Double.Epsilon perderá su resolución. Parece que la mejor manera de hacerlo es hacer un poco de cambio.

SOLUCIÓN:

[TestMethod] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void CreateExtent_InvalidTop_ShouldThrowArgumentOutOfRangeException() { var invalidTop = Utility.IncrementTiny(90); // 90.000000000000014 // var sameAsEpsilon = Utility.IncrementTiny(0); new Extent(invalidTop, 0, 0, 0); } /// <summary> /// Increment a double-precision number by the smallest amount possible /// </summary> /// <param name="number">double-precision number</param> /// <returns>incremented number</returns> public static double IncrementTiny(double number) { #region SANITY CHECKS if (Double.IsNaN(number) || Double.IsInfinity(number)) throw new ArgumentOutOfRangeException("number"); #endregion var bits = BitConverter.DoubleToInt64Bits(number); // if negative then go opposite way if (number > 0) return BitConverter.Int64BitsToDouble(bits + 1); else if (number < 0) return BitConverter.Int64BitsToDouble(bits - 1); else return Double.Epsilon; } /// <summary> /// Decrement a double-precision number by the smallest amount possible /// </summary> /// <param name="number">double-precision number</param> /// <returns>decremented number</returns> public static double DecrementTiny(double number) { #region SANITY CHECKS if (Double.IsNaN(number) || Double.IsInfinity(number)) throw new ArgumentOutOfRangeException("number"); #endregion var bits = BitConverter.DoubleToInt64Bits(number); // if negative then go opposite way if (number > 0) return BitConverter.Int64BitsToDouble(bits - 1); else if (number < 0) return BitConverter.Int64BitsToDouble(bits + 1); else return 0 - Double.Epsilon; }

Esto hace el trabajo.


Porque Double.Epsilon es el "cambio más pequeño que se nota" (en términos generales) en un número doble.

... pero esto no significa que tendrá ningún efecto cuando lo uses.

Como saben, los flotadores / dobles varían en su resolución dependiendo de la magnitud del valor que contienen. Por ejemplo, artificial:

  • ...
  • -100 -> + -0.1
  • -10 -> + -0.01
  • 0 -> + -0.001
  • 10 -> + -0.01
  • 100 -> + -0.1
  • ...

Si las resoluciones fueran así, el Epsilon sería 0.001 , ya que es el cambio más pequeño posible. Pero, ¿cuál sería el resultado esperado de 1000000 + 0.001 en dicho sistema?


Según la documentación de Double.Epsilon :

El valor de la propiedad Epsilon refleja el valor Double positivo más pequeño que es significativo en operaciones numéricas o comparaciones cuando el valor de la instancia Double es cero .

(Énfasis mío.)

Agregarlo a 90.0 no produce "el siguiente valor más pequeño después de 90.0", esto solo produce 90.0 de nuevo.


Double.Epsilon es el valor representable positivo más pequeño. El hecho de que se pueda representar por sí solo no significa que sea el valor más pequeño entre cualquier otro valor representable y el siguiente valor más alto.

Imagina que tienes un sistema para representar solo enteros. Puede representar cualquier número entero a 5 cifras significativas, junto con una escala (por ejemplo, en el rango 1-100).

Así que estos valores son exactamente representables, por ejemplo.

  • 12345 (dígitos = 12345, escala = 0)
  • 12345000 (dígitos = 12345, escala = 3)

En ese sistema, el valor de "épsilon" sería 1 ... pero si agrega 1 a 12345000, terminaría con 12345000 porque el sistema no podría representar el resultado exacto de 12345001.

Ahora aplique la misma lógica para double , con todas sus complejidades, y obtendrá un épsilon mucho más pequeño, pero el mismo principio general: un valor que es distinto de cero, pero que puede terminar sin hacer ninguna diferencia cuando se agrega a números más grandes.

Tenga en cuenta que los valores mucho más grandes también tienen la misma propiedad; por ejemplo, si x es un double muy grande, entonces x + 1 bien puede ser igual a x porque la brecha entre dos dobles "adyacentes" se vuelve más de 2 a medida que los valores aumentan. .