Ошибка округления в два раза в методе .NET Double.ToString

Математически рассмотрим для этого вопроса рациональное число

8725724278030350 / 2**48

где** в знаменателе обозначает возведение в степень, то есть знаменатель2 к48сила (Фракция не в низших терминах, сводится к 2.) Это числоexactly представляемый какSystem.Double, Его десятичное расширение

31.0000000000000'49'73799150320701301097869873046875 (exact)

где апострофы не представляют пропущенные цифры, а просто обозначают границы, где округление до15 соответственно17 цифры должны быть выполнены.

Обратите внимание на следующее: если это число округлено до 15 цифр, результат будет31 (затем тринадцать0s) потому что следующие цифры (49...) начать с4 (имеется в виду раундdown). Но если числоfirst округляется до 17 цифр иthen округляется до 15 цифр, результат может быть31.0000000000001, Это потому, что первое округление округляется путем увеличения49... цифры до50 (terminates) (следующие цифры были73...), и второе округление может затем снова округляться (когда правило округления средней точки говорит «округление от нуля»).

(Конечно, есть еще много чисел с вышеуказанными характеристиками.)

Теперь оказывается, что стандартное строковое представление .NET этого числа"31.0000000000001". The question: Isn't this a bug? Под стандартным строковым представлением мы подразумеваемString производится параметрамиDouble.ToString() метод экземпляра, который, конечно, идентичен тому, что производитсяToString("G").

Интересно отметить, что если вы приведете вышеуказанное число кSystem.Decimal тогда вы получитеdecimal то есть31 именно так! Увидетьэтот вопрос переполнения стека для обсуждения удивительного факта, что кастингDouble вDecimal включает в себя первое округление до 15 цифр. Это означает, что приведение кDecimal делает правильный раунд до 15 цифр, тогда как коллToSting() делает неправильный.

Подводя итог, мы имеем число с плавающей запятой, которое при выводе пользователю31.0000000000001, но при конвертации вDecimal (где29 цифры доступны), становится31 именно так. Это неудачно.

Вот некоторый код C # для вас, чтобы проверить проблему:

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
}

В приведенном выше коде используются SkeetToExactString метод. Если вы не хотите использовать его материалы (можно найти по URL), просто удалите строки кода выше в зависимости отexactString, Вы все еще можете увидеть, какDouble обсуждаемый (evil) округлен и отлит.

ADDITION:

Итак, я проверил еще несколько чисел, и вот таблица:

  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

Первый столбец дает точное (хотя и усеченное) значение, котороеDouble представлять. Второй столбец дает строковое представление из"R" строка формата. Третий столбец дает обычное строковое представление. И, наконец, четвертый столбец даетSystem.Decimal что является результатом преобразования этогоDouble.

Мы заключаем следующее:

Round to 15 digits by ToString() and round to 15 digits by conversion to Decimal disagree in very many cases Conversion to Decimal also rounds incorrectly in many cases, and the errors in these cases cannot be described as "round-twice" errors In my cases, ToString() seems to yield a bigger number than Decimal conversion when they disagree (no matter which of the two rounds correctly)

Я экспериментировал только с такими случаями, как указано выше. Я не проверял, есть ли ошибки округления с номерами других "форм".

Ответы на вопрос(4)

Ваш ответ на вопрос