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
al48
el 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 trece0
s) 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".