关于c#:舍入双精度中的重要数字,而不是小数位

Rounding the SIGNIFICANT digits in a double, not to decimal places

本问题已经有最佳答案,请猛点这里访问。

我需要把双打的有效数字四舍五入。例子圆形(1.2e-20,0)应变为1.0e-20

我不能使用Math.Round(1.2e-20,0),它返回0,因为Math.Round()不将浮点中的有效数字舍入,而是将其舍入到十进制数字,即E为0的双精度数。

当然,我可以这样做:

1
2
3
4
double d = 1.29E-20;
d *= 1E+20;
d = Math.Round(d, 1);
d /= 1E+20;

实际上是有效的。但这并不是:

1
2
3
4
d = 1.29E-10;
d *= 1E+10;
d = Math.Round(d, 1);
d /= 1E+10;

在这种情况下,d是0.0000000000130000000000002。问题是,double在内部存储2的分数,这与10的分数不完全匹配。在第一种情况下,C似乎只处理*和/的指数,但在第二种情况下,它会进行实际的*或/操作,从而导致问题。

当然,我需要一个总是给出正确结果的公式,不仅是有时。

也就是说,我不应该在舍入后使用任何双精度运算,因为双精度运算不能精确地处理小数。

上述计算的另一个问题是,没有返回双精度数指数的双精度函数。当然,可以使用数学库来计算它,但是很难保证它始终与双内部代码具有完全相同的结果。

在绝望中,我考虑将一个双精度数转换成一个字符串,找到有效数字,进行四舍五入,然后将四舍五入的数字转换成一个字符串,最后将这个数字转换成双精度数。丑陋,对吧?在所有情况下也可能无法正常工作:-(

是否有任何库或建议如何正确舍入双精度数的有效位数?

附:在声明这是一个重复的问题之前,请确保您理解有效数字和小数点之间的区别。


The problem is that double stores internally fractions of 2, which cannot match exactly fractions of 10

这是个问题,是的。如果它在您的场景中很重要,您需要使用一个以十进制而不是二进制存储数字的数字类型。在.NET中,该数字类型是decimal

注意,对于许多计算任务(但不是货币,例如),double类型是可以的。事实上,您没有得到您要寻找的值,这与使用double时存在的任何其他舍入错误相比,都是一个问题。

还要注意,如果唯一的目的是显示数字,您甚至不需要自己进行四舍五入。您可以使用自定义数字格式来完成相同的操作。例如:

1
2
double value = 1.29e-10d;
Console.WriteLine(value.ToString("0.0E+0"));

显示字符串1.3E-10

Another problem with the calculation above is that there is no double function returning the exponent of a double

我不知道你的意思。Math.Log10()方法正好做到了这一点。当然,它返回给定数字的精确指数,以10为底。根据您的需要,您实际上更喜欢Math.Floor(Math.Log10(value)),它提供了以科学记数法显示的指数值。

it might be difficult to guarantee that this has always precisely the same result as the double internal code

由于double的内部存储使用IEEE二进制格式,其中指数和尾数都存储为二进制数,因此显示的指数基数10无论如何都不会"与双内部代码完全相同"。当然,指数是一个整数,可以精确表示。但它不像一个十进制值被存储在第一位。

在任何情况下,Math.Log10()都会返回一个有用的值。

Is there any library or any suggestion how to round the significant digits of a double properly ?

如果你只是为了显示而需要取整,那就不要做任何数学运算。只需使用自定义数字格式字符串(如上所述)来按照您想要的方式格式化值。

如果你真的需要自己做四舍五入,那么根据你的描述,我认为下面的方法应该有效:

1
2
3
4
5
6
7
8
9
10
static double RoundSignificant(double value, int digits)
{
    int log10 = (int)Math.Floor(Math.Log10(value));
    double exp = Math.Pow(10, log10);
    value /= exp;
    value = Math.Round(value, digits);
    value *= exp;

    return value;
}