Ошибка округления в два раза в методе .NET Double.ToString
Математически рассмотрим для этого вопроса рациональное число
8725724278030350 / 2**48
где**
в знаменателе обозначает возведение в степень, то есть знаменатель2
к48
сила (Фракция не в низших терминах, сводится к 2.) Это числоexactly представляемый какSystem.Double
, Его десятичное расширение
31.0000000000000'49'73799150320701301097869873046875 (exact)
где апострофы не представляют пропущенные цифры, а просто обозначают границы, где округление до15 соответственно17 цифры должны быть выполнены.
Обратите внимание на следующее: если это число округлено до 15 цифр, результат будет31
(затем тринадцать0
s) потому что следующие цифры (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 byToString()
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)
Я экспериментировал только с такими случаями, как указано выше. Я не проверял, есть ли ошибки округления с номерами других "форм".