Форматирование удваивается для вывода в С#

Запуск быстрого эксперимента, связанного с Является ли двойное умножение разбитым в .NET? и прочитав пару статей о форматировании строк в С#, я подумал, что это:

{
    double i = 10 * 0.69;
    Console.WriteLine(i);
    Console.WriteLine(String.Format("  {0:F20}", i));
    Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
    Console.WriteLine(String.Format("= {0:F20}", 6.9));
}

Был бы эквивалент С# этого кода C:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}

Однако С# выдает результат:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000

несмотря на то, что я показываю значение, равное значению 6.89999999999999946709 (а не 6.9) в отладчике.

по сравнению с C, который показывает точность, запрошенную в формате:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          

Что происходит?

(Microsoft.NET Framework версии 3.51 с пакетом обновления 1 (SP1)/Visual Studio С# 2008 Express Edition)


У меня есть фон в численных вычислениях и опыт реализации интервальной арифметики - метод оценки ошибок из-за пределов точности в сложных числовых системах - на разных платформах. Чтобы получить награду, не пытайтесь объяснить точность хранения - в этом случае это разница в одном ULP 64-битного двойника.

Чтобы получить щедрость, я хочу знать, как (или нет).Net может форматировать двойную требуемую точность как видимую в коде C.

Ответ 1

Проблема заключается в том, что .NET всегда будет округлять от double до 15 значащих десятичных цифр до применения вашего форматирования независимо от точности, запрашиваемой вашим форматом, и независимо от точного десятичного значения двоичного числа.

Я предполагаю, что отладчик Visual Studio имеет свои собственные процедуры отображения/отображения, которые напрямую обращаются к внутреннему двоичному номеру, следовательно, расхождения между вашим кодом С#, вашим кодом C и отладчиком.

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

В качестве альтернативы вы можете использовать Jon Skeet DoubleConverter class (связанный с его "Binary floating point и .NET" , статья). Это имеет метод ToExactString, который возвращает точное десятичное значение a double. Вы можете легко изменить это, чтобы обеспечить округление вывода до определенной точности.

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625

Ответ 2

Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);

Ответ 3

Посмотрите на ссылку MSDN. В примечаниях указано, что числа округлены до количества запрошенных десятичных знаков.

Если вместо этого вы используете "{0: R}", он произведет то, что называется значением "round-trip", взгляните на это MSDN ссылка для получения дополнительной информации, здесь мой код и вывод:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

Выход

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000

Ответ 4

Хотя этот вопрос пока закрыт, я считаю, что стоит упомянуть, как это зверство возникло. В некотором смысле вы можете обвинить спецификацию С#, которая гласит, что double должен иметь точность 15 или 16 цифр (результат IEEE-754). Немного дальше (раздел 4.1.6) он заявил, что реализациям разрешено использовать более высокую точность. Имейте в виду: выше, а не ниже. Им даже разрешено отклоняться от IEEE-754: выражения типа x * y / z, где x * y будут давать +/-INF, но будут иметь допустимый диапазон после деления, не должны приводить к ошибке. Эта функция упрощает компиляторы использовать более высокую точность в архитектурах, где это даст лучшую производительность.

Но я пообещал "разум". Здесь цитата (вы запросили ресурс в одном из своих последних комментариев) из общего источника CLI, в clr/src/vm/comnumber.cpp:

"Чтобы указать числа, которые оба удобный для отображения и round-trippable, мы анализируем число используя 15 цифр, а затем определите, он округляет поездки до того же значения. Если он делает это, мы преобразуем этот NUMBER в string, иначе мы перепишем с помощью 17 цифры и отобразить это."

Другими словами: команда разработчиков MS CLI решила быть как с округлостью, так и с хорошими значениями, которые не так больно читать. Хорошо или плохо? Я хотел бы отказаться или отказаться.

Трюк, который он делает, чтобы узнать эту циклическую возможность любого числа? Преобразование в общую структуру NUMBER (которая имеет отдельные поля для свойств двойника) и обратно, а затем сравнить, отличается ли результат. Если оно отличается, то точное значение используется (как в вашем среднем значении с 6.9 - i), если оно то же самое, используется "симпатичное значение".

Как вы уже отметили в комментарии к Andyp, 6.90...00 побитно равно 6.89...9467. И теперь вы знаете, почему используется 0.0...8818: побитовое отличие от 0.0.

Этот 15-значный барьер жестко закодирован и может быть изменен только путем перекомпиляции CLI с помощью Mono или путем вызова Microsoft и убеждения их добавить параметр для печати полной "точности" (это не очень точно, но отсутствие лучшего слова). Вероятно, проще просто вычислить точность в 52 бита или использовать ранее упомянутую библиотеку.

EDIT: если вы хотите поэкспериментировать с плавающими точками IEE-754, рассмотреть этот онлайн-инструмент, в котором показаны все соответствующие части с плавающей запятой.

Ответ 5

Использование

Console.WriteLine(String.Format("  {0:G17}", i));

Это даст вам все 17 цифр. По умолчанию двойное значение содержит 15 десятичных цифр точности, хотя не более 17 цифр поддерживается внутри. {0: R} не всегда даст вам 17 цифр, это даст 15, если число может быть представлено с такой точностью.

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

В хранилище .NET double can not больше цифр, чем 17, поэтому вы не можете увидеть 6.89999999999999946709 в отладчике, который вы увидите 6.8999999999999995. Пожалуйста, предоставьте изображение, чтобы доказать, что мы ошибаемся.

Ответ 6

Ответ на этот вопрос прост и можно найти на MSDN

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

В вашем примере значение я равно 6.89999999999999946709, которое имеет номер 9 для всех позиций между 3-й и 16-й цифрами (не забудьте подсчитать целую часть в цифрах). При преобразовании в строку фреймворк округляет число до 15-й цифры.

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901

Ответ 7

Я попытался воспроизвести ваши результаты, но когда я смотрел "i" в отладчике, он появился как "6.8999999999999995" не так: "6.89999999999999946709", как вы писали в вопросе. Можете ли вы предоставить шаги для воспроизведения того, что вы видели?

Чтобы увидеть, что показывает отладчик, вы можете использовать DoubleConverter, как в следующей строке кода:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

Надеюсь, это поможет!

Изменить: я думаю, что я устал, чем думал, конечно, это то же самое, что форматирование в значение округления (как упоминалось ранее).

Ответ 8

Ответ: да, двойная печать нарушена в .NET, они печатают конечные цифры мусора.

Вы можете прочитать, как правильно его реализовать здесь.

Мне пришлось сделать то же самое для IronScheme.

> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995

Примечание. Оба C и С# имеют правильное значение, только сломанная печать.

Обновление: я все еще ищу список рассылки, который у меня был, что привело к этому открытию.

Ответ 9

Я нашел это быстрое исправление.

    double i = 10 * 0.69;
    System.Diagnostics.Debug.WriteLine(i);


    String s = String.Format("{0:F20}", i).Substring(0,20);
    System.Diagnostics.Debug.WriteLine(s + " " +s.Length );

Ответ 10

Console.WriteLine(string.Format( "Стоимость курса: {0: 0.00}", + cfees));

internal void DisplaycourseDetails()
        {
            Console.WriteLine("Course Code : " + cid);
            Console.WriteLine("Couse Name : " + cname);
            //Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees));
            // string s = string.Format("Course Fees is {0:0.00}", +cfees);
            // Console.WriteLine(s);
            Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees));
        }
        static void Main(string[] args)
        {
            Course obj1 = new Course(101, "C# .net", 1100.00);
            obj1.DisplaycourseDetails();
            Course obj2 = new Course(102, "Angular", 7000.00);
            obj2.DisplaycourseDetails();
            Course obj3 = new Course(103, "MVC", 1100.00);
            obj3.DisplaycourseDetails();
            Console.ReadLine();
        }
    }