Erro de duas voltas no método Double.ToString do .NET
Matematicamente, considere para esta questão o número racional
8725724278030350 / 2**48
Onde**
no denominador denota exponenciação, ou seja, o denominador é2
ao48
poder de th. (A fração não está em termos mais baixos, é redutível por 2.) Esse número éexatamente representável comoSystem.Double
. Sua expansão decimal é
31.0000000000000'49'73799150320701301097869873046875 (exact)
onde os apóstrofos não representam dígitos faltantes, mas apenas marcam os boudários onde o arredondamento15 resp.17 dígitos deve ser executado.
Observe o seguinte: Se este número for arredondado para 15 dígitos, o resultado será31
(seguido por treze0
s) porque os próximos dígitos (49...
começar com um4
(que significa rodadabaixa). Mas se o número forprimeiro arredondado para 17 dígitos eentão arredondado para 15 dígitos, o resultado pode ser31.0000000000001
. Isso ocorre porque o primeiro arredondamento aumenta aumentando49...
dígitos para50 (terminates)
(os próximos dígitos foram73...
), e o segundo arredondamento pode então ser arredondado novamente (quando a regra de arredondamento do ponto médio diz "arredonda para zero").
(Existem muitos mais números com as características acima, é claro).
Agora, verifica-se que a representação de string padrão do .NET desse número é"31.0000000000001"
. A pergunta: isso não é um bug? Por representação de string padrão queremos dizerString
produzido pelos parâmetrosDouble.ToString()
exemplo método que é, naturalmente, idêntico ao que é produzido porToString("G")
.
Uma coisa interessante a notar é que se você lançar o número acima paraSystem.Decimal
então você ganha umdecimal
isso é31
exatamente! Vejoesta pergunta de estouro de pilha para uma discussão sobre o fato surpreendente de que lançar umDouble
paraDecimal
envolve primeiro arredondamento para 15 dígitos. Isso significa que a transmissão paraDecimal
faz um arredondamento correto para 15 dígitos, enquantoToSting()
faz um incorreto.
Para resumir, temos um número de ponto flutuante que, quando a saída para o usuário, é31.0000000000001
, mas quando convertido paraDecimal
(Onde29 dígitos estão disponíveis), torna-se31
exatamente. Isto é um infortúnio.
Aqui está um código c # para você verificar o 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
}
O código acima usa o Skeet'sToExactString
método. Se você não quer usar o material dele (pode ser encontrado através da URL), apenas apague as linhas de código acima dependentesexactString
. Você ainda pode ver como oDouble
em questão (evil
) é arredondado e lançado.
ADIÇÃO:
OK, então testei mais alguns números e aqui está uma tabela:
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
A primeira coluna fornece o valor exato (embora truncado) que oDouble
representar. A segunda coluna fornece a representação de string do"R"
format string. A terceira coluna fornece a representação usual de string. E finalmente a quarta coluna dá aSystem.Decimal
que resulta da conversão desteDouble
.
Nós concluímos o seguinte:
Arredondar para 15 dígitos porToString()
e arredondar para 15 dígitos por conversão paraDecimal
discordo em muitos casosConversão paraDecimal
também arredonda incorretamente em muitos casos, e os erros nesses casos não podem ser descritos como erros "round-twice"Nos meus casos,ToString()
parece render um número maior do queDecimal
conversão quando eles discordam (não importa qual das duas rodadas corretamente)Eu só experimentei casos como os acima. Não verifiquei se há erros de arredondamento com números de outros "formulários".