Error de redondeo dos veces en el método Double.ToString de .NET

Matemáticamente, considere para esta pregunta el número racional

8725724278030350 / 2**48

dónde** en el denominador denota exponenciación, es decir, el denominador es2 al48el poder (La fracción no está en los términos más bajos, reducibles en 2). Este número esexactamente representable comoSystem.Double. Su expansión decimal es.

31.0000000000000'49'73799150320701301097869873046875 (exact)

donde los apóstrofes no representan dígitos faltantes, sino simplemente marcan los boudarios donde se redondea a15 resp.17 Los dígitos deben ser realizados.

Tenga en cuenta lo siguiente: Si este número se redondea a 15 dígitos, el resultado será31 (seguido de trece0s) porque los siguientes dígitos (49...) comienza con un4 (que significa rondaabajo). Pero si el número esprimero redondeado a 17 dígitos yentonces redondeado a 15 dígitos, el resultado podría ser31.0000000000001. Esto se debe a que el primer redondeo se redondea al aumentar el49... dígitos para50 (terminates) (los siguientes dígitos fueron73...), y el segundo redondeo podría entonces redondear nuevamente (cuando la regla de redondeo del punto medio dice "redondear fuera de cero").

(Hay muchos más números con las características anteriores, por supuesto).

Ahora, resulta que la representación de cadena estándar de .NET de este número es"31.0000000000001". La pregunta: ¿No es esto un error? Por representación de cadena estándar nos referimos a laString producido por los parametrosDouble.ToString() método de instancia que es, por supuesto, idéntico a lo que se produce porToString("G").

Una cosa interesante a tener en cuenta es que si lanza el número anterior aSystem.Decimal entonces obtienes undecimal es decir31 ¡exactamente! Veresta pregunta de desbordamiento de pila para una discusión del sorprendente hecho de que lanzar unDouble aDecimal Implica el primer redondeo a 15 dígitos. Esto significa que el casting aDecimal hace una ronda correcta a 15 dígitos, mientras que llamandoToSting() hace una incorrecta.

Para resumir, tenemos un número de punto flotante que, cuando se envía al usuario, es31.0000000000001, pero cuando se convierte aDecimal (dónde29 dígitos están disponibles), se convierte en31 exactamente. Esto es desafortunado.

Aquí hay un código C # para que verifiques el problema:

static void Main()
{
  const double evil = 31.0000000000000497;
  string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx 

  Console.WriteLine("Exact value (Jon Skeet): {0}", exactString);   // writes 31.00000000000004973799150320701301097869873046875
  Console.WriteLine("General format (G): {0}", evil);               // writes 31.0000000000001
  Console.WriteLine("Round-trip format (R): {0:R}", evil);          // writes 31.00000000000005

  Console.WriteLine();
  Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));

  Console.WriteLine();
  decimal converted = (decimal)evil;
  Console.WriteLine("Decimal version: {0}", converted);             // writes 31
  decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
  Console.WriteLine("Better decimal: {0}", preciseDecimal);         // writes 31.000000000000049737991503207
}

El código anterior usa Skeet'sToExactString método. Si no quiere usar sus cosas (se pueden encontrar a través de la URL), simplemente elimine las líneas de código de arriba dependiendo deexactString. Todavía se puede ver cómoDouble en cuestión (evil) es redondeado y fundido.

ADICIÓN:

OK, entonces probé algunos números más, y aquí hay una tabla:

  exact value (truncated)       "R" format         "G" format     decimal cast
 -------------------------  ------------------  ----------------  ------------
 6.00000000000000'53'29...  6.0000000000000053  6.00000000000001  6
 9.00000000000000'53'29...  9.0000000000000053  9.00000000000001  9
 30.0000000000000'49'73...  30.00000000000005   30.0000000000001  30
 50.0000000000000'49'73...  50.00000000000005   50.0000000000001  50
 200.000000000000'51'15...  200.00000000000051  200.000000000001  200
 500.000000000000'51'15...  500.00000000000051  500.000000000001  500
 1020.00000000000'50'02...  1020.000000000005   1020.00000000001  1020
 2000.00000000000'50'02...  2000.000000000005   2000.00000000001  2000
 3000.00000000000'50'02...  3000.000000000005   3000.00000000001  3000
 9000.00000000000'54'56...  9000.0000000000055  9000.00000000001  9000
 20000.0000000000'50'93...  20000.000000000051  20000.0000000001  20000
 50000.0000000000'50'93...  50000.000000000051  50000.0000000001  50000
 500000.000000000'52'38...  500000.00000000052  500000.000000001  500000
 1020000.00000000'50'05...  1020000.000000005   1020000.00000001  1020000

La primera columna da el valor exacto (aunque truncado) que elDouble representar. La segunda columna da la representación de cadena de la"R" cadena de formato. La tercera columna da la representación de cadena habitual. Y finalmente la cuarta columna da laSystem.Decimal que resulta de convertir estoDouble.

Concluimos lo siguiente:

Redondear a 15 dígitos porToString() y redondear a 15 dígitos por conversión aDecimal en desacuerdo en muchos casosConversión aDecimal también redondea incorrectamente en muchos casos, y los errores en estos casos no se pueden describir como errores "redondeados dos veces"En mis casos,ToString() parece producir un número mayor queDecimal conversión cuando no están de acuerdo (no importa cuál de las dos rondas correctamente)

Solo experimenté con casos como los anteriores. No he comprobado si hay errores de redondeo con números de otros "formularios".

Respuestas a la pregunta(4)

Su respuesta a la pregunta