Dwukrotny błąd w metodzie Double.ToString .NET

Matematycznie rozważmy dla tego pytania liczbę wymierną

8725724278030350 / 2**48

gdzie** w mianowniku oznacza potęgowanie, tj. mianownik jest2 do48potęga. (Ułamek nie jest najniższy, można go zmniejszyć o 2.) Ta liczba wynosidokładnie reprezentowalny jakoSystem.Double. Jego dziesiętna ekspansja jest

31.0000000000000'49'73799150320701301097869873046875 (exact)

gdzie apostrofy nie reprezentują brakujących cyfr, a jedynie zaznaczają krzyżyki, do których są zaokrąglone15 wzgl.17 cyfry należy wykonać.

Zwróć uwagę na następujące kwestie: Jeśli liczba ta zostanie zaokrąglona do 15 cyfr, wynikiem będzie31 (następnie trzynaście0s) ponieważ następne cyfry (49...) zacznij od a4 (co oznacza rundana dół). Ale jeśli liczba jestpierwszy zaokrąglone do 17 cyfr inastępnie zaokrąglone do 15 cyfr, wynik może być31.0000000000001. To dlatego, że pierwsze zaokrąglenie zaokrągla się w górę, zwiększając49... cyfry do50 (terminates) (następne cyfry były73...), a drugie zaokrąglenie może zaokrąglić się ponownie (gdy reguła zaokrąglania punktu środkowego mówi „zaokrąglić od zera”).

(Oczywiście jest wiele więcej liczb o powyższych cechach).

Teraz okazuje się, że standardowa ciąg znaków reprezentujący tę liczbę .NET"31.0000000000001". Pytanie: czy to nie błąd? Przez standardową reprezentację ciągu rozumiemyString produkowane przez parametryDouble.ToString() metoda instancji, która jest oczywiście identyczna z produkowaną przezToString("G").

Interesującą rzeczą do odnotowania jest to, że jeśli rzucisz powyższy numer naSystem.Decimal wtedy dostanieszdecimal to jest31 dokładnie! Widziećto pytanie o przepełnienie stosu do dyskusji na temat zaskakującego faktu, że rzucanieDouble doDecimal obejmuje pierwsze zaokrąglenie do 15 cyfr. Oznacza to, że casting doDecimal tworzy poprawną rundę na 15 cyfr, podczas gdy wywołanieToSting() robi niepoprawny.

Podsumowując, mamy liczbę zmiennoprzecinkową, która, gdy dane wyjściowe dla użytkownika, to31.0000000000001, ale po konwersji naDecimal (gdzie29 cyfry są dostępne), staje się31 dokładnie. To niefortunne.

Oto kod C #, aby zweryfikować problem:

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
}

Powyższy kod używa SkeetaToExactString metoda. Jeśli nie chcesz używać jego rzeczy (można znaleźć za pośrednictwem adresu URL), po prostu usuń wiersze kodu powyżej zależneexactString. Nadal możesz zobaczyć, jakDouble w pytaniu (evil) jest zaokrąglony i odlany.

DODANIE:

OK, przetestowałem więcej liczb i oto 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

Pierwsza kolumna podaje dokładną (choć obciętą) wartość, którąDouble przedstawiać. Druga kolumna podaje reprezentację ciągu z"R" formatuj ciąg. Trzecia kolumna podaje zwykłą reprezentację ciągu. I wreszcie czwarta kolumna podajeSystem.Decimal to wynika z konwersji tegoDouble.

Podsumowujemy:

Zaokrąglij do 15 cyfr poToString() i zaokrąglaj do 15 cyfr po konwersji naDecimal w wielu przypadkach nie zgadzam sięKonwersja doDecimal w wielu przypadkach również zaokrągla błędnie, a błędów w tych przypadkach nie można opisać jako błędów „dwa razy”W moich przypadkachToString() wydaje się dawać większą liczbę niżDecimal konwersja, gdy nie zgadzają się (bez względu na to, która z dwóch rund poprawnie)

Eksperymentowałem tylko z takimi przypadkami jak powyżej. Nie sprawdziłem, czy występują błędy zaokrąglania z liczbą innych „formularzy”.

questionAnswers(4)

yourAnswerToTheQuestion