格式化C#中输出的双精度数

时间:2009-09-14 13:23:38

标签: c# formatting floating-point ieee-754

运行与Is double Multiplication Broken in .NET?相关的快速实验并阅读有关C#字符串格式的几篇文章,我认为这是:

{
    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代码的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 );
}

然而,C#产生输出:

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 SP1 / Visual Studio C#2008 Express Edition)


我有数值计算的背景和经验实现区间算术 - 一种在各种平台上估算由于复杂数值系统的精度限制而导致的误差的技术。要获得赏金,请不要尝试解释存储精度 - 在这种情况下,它是64位双精度的一个ULP的差异。

为了得到赏金,我想知道.Net是如何(或是否)将双倍格式化为C代码中可见的请求精度。

11 个答案:

答案 0 :(得分:57)

问题是,在应用格式之前,.NET将始终将double舍入到15个有效小数位,无论格式请求的精度如何,也不管精确的十进制值如何二进制数。

我猜测Visual Studio调试器有自己的格式/显示例程,可以直接访问内部二进制数,因此C#代码,C代码和调试器之间存在差异。

没有任何内置功能可以让您访问double的确切十进制值,或者您可以将double格式化为特定数量的小数位,但您可以执行此操作这是你自己挑选内部二进制数并将其重建为十进制值的字符串表示。

或者,您可以使用Jon Skeet的DoubleConverter class(从他的"Binary floating point and .NET" article链接)。这有一个ToExactString方法,它返回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

答案 1 :(得分:21)

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);

答案 2 :(得分:8)

看看这个MSDN reference。它在说明中指出数字四舍五入到所请求的小数位数。

如果您使用“{0:R}”,它将产生所谓的“往返”值,请查看此MSDN reference以获取更多信息,这是我的代码和输出:

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

答案 3 :(得分:8)

虽然这个问题同时关闭,但我相信值得一提的是这种暴行是如何形成的。在某种程度上,你可能会责怪C#规范,它规定双精度必须具有15或16位数的精度(IEEE-754的结果)。更进一步(第4.1.6节),它声明允许实现使用更高精度。请注意:更高,而不是更低。它们甚至被允许偏离IEEE-754:x * y / z类型的表达式x * y将产生+/-INF但是在划分后将处于有效范围内,不必导致错误。此功能使编译器更容易在体系结构中使用更高的精度,从而产生更好的性能。

但我答应了“理由”。以下是clr/src/vm/comnumber.cpp6.9 - i的{​​{1}}引用(您在最近的一条评论中请求了资源):

  

“为了给出两者都有的数字   友好的展示和   圆形的,我们解析数字   使用15位数,然后确定是否   它往返相同的价值。如果   它确实如此,我们将该NUMBER转换为a   字符串,否则我们使用17重新分析   数字和显示。“

换句话说:MS的CLI开发团队决定既可以进行循环播放,也可以显示非常难以理解的值。是好是坏?我希望选择加入或选择退出。

它可以找出任何给定数字的这种圆形可能性吗?转换为通用NUMBER结构(具有双重属性的单独字段)和返回,然后比较结果是否不同。如果它不同,则使用确切的值(如同6.90...00的中间值)如果相同,则使用“漂亮值”。

正如您在对Andyp的评论中所说,6.89...9467在位上等于0.0...8818。现在您知道为什么使用0.0:它与{{1}}略有不同。

15位屏障是硬编码的,只能通过重新编译CLI,使用Mono或通过调用Microsoft并说服他们添加选项来打印完整的“精度”来更改(它)不是真的精确,但缺乏一个更好的词)。可能更容易自己计算52位精度或使用前面提到的库。

编辑:如果您想尝试使用IEE-754浮点Shared Source CLI,它会显示浮点的所有相关部分。

答案 4 :(得分:4)

使用

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

这将为您提供所有17位数字。默认情况下,Double值包含15个十进制数字的精度,但内部最多保留17位数。 {0:R}不会总是给你17位数字,如果数字可以用那个精度表示,它将给出15位数。

如果数字可以用该精度表示,则返回15位数;如果数字只能用最大精度表示,则返回17位数。没有任何事情可以使双重返回更多的数字,这是它的实现方式。如果你不喜欢它自己做一个新的双课......

.NET的双重存储不能存储超过17的数字,所以你不能在调试器中看到6.89999999999999946709,你会看到6.8999999999999995。请提供一张图片来证明我们的错误。

答案 5 :(得分:2)

答案很简单,可以在MSDN

找到
  

请记住,浮点数只能近似一个十进制数,并且浮点数的精度决定了该数字接近十进制数的准确程度。默认情况下,Double值包含 15位精度的十进制数字,但内部最多保留17位数。

在您的示例中,i的值为6.89999999999999946709,对于第3位和第16位之间的所有位置,其值为9(记住计算数字中的整数部分)。转换为字符串时,框架将数字四舍五入到第15位。

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901

答案 6 :(得分:2)

我试图重现你的发现,但当我在调试器中看到'i'时,它显示为'6.8999999999999995'而不是你在问题中写的'6.89999999999999946709'。你能提供重现所见内容的步骤吗?

要查看调试器显示的内容,您可以使用DoubleConverter,如以下代码行所示:

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

希望这有帮助!

编辑:我想我比我想的更累,当然这与格式化到往返值相同(如前所述)。

答案 7 :(得分:0)

答案是肯定的,在.NET中打破双重打印,它们正在打印尾随垃圾数字。

您可以阅读如何正确实施here

我必须为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和C#都有正确的值,只是打破了打印。

更新:我仍然在寻找导致这一发现的邮件列表对话。

答案 8 :(得分:0)

我发现了这个快速修复。

    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 );

答案 9 :(得分:0)

另一种方法,从该方法开始:

double i = (10 * 0.69);
Console.Write(ToStringFull(i));       // Output 6.89999999999999946709294817
Console.Write(ToStringFull(-6.9)      // Output -6.90000000000000035527136788
Console.Write(ToStringFull(i - 6.9)); // Output -0.00000000000000088817841970012523233890533

插入功能...

public static string ToStringFull(double value)
{
    if (value == 0.0) return "0.0";
    if (double.IsNaN(value)) return "NaN";
    if (double.IsNegativeInfinity(value)) return "-Inf";
    if (double.IsPositiveInfinity(value)) return "+Inf";

    long bits = BitConverter.DoubleToInt64Bits(value);
    BigInteger mantissa = (bits & 0xfffffffffffffL) | 0x10000000000000L;
    int exp = (int)((bits >> 52) & 0x7ffL) - 1023;
    string sign = (value < 0) ? "-" : "";

    if (54 > exp)
    {
        double offset = (exp / 3.321928094887362358); //...or =Math.Log10(Math.Abs(value))
        BigInteger temp = mantissa * BigInteger.Pow(10, 26 - (int)offset) >> (52 - exp);
        string numberText = temp.ToString();
        int digitsNeeded = (int)((numberText[0] - '5') / 10.0 - offset);
        if (exp < 0)
            return sign + "0." + new string('0', digitsNeeded) + numberText;
        else
            return sign + numberText.Insert(1 - digitsNeeded, ".");
    }
    return sign + (mantissa >> (52 - exp)).ToString();
}

工作原理

为解决此问题,我使用了BigInteger工具。大值很简单,因为它们只需要将尾数左移指数即可。对于较小的值,我们不能直接右移,因为那样会丢失精度位。我们必须先将其乘以10 ^ n,再给它一些额外的大小,然后再进行右移。之后,我们将小数点移到左边的n个小数位。更多文本/代码here

答案 10 :(得分:-3)

Console.WriteLine(string.Format(&#34;课程费用为{0:0.00}&#34;,+ 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();
        }
    }