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 ao48poder 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 treze0s) 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".

questionAnswers(4)

yourAnswerToTheQuestion