【问题标题】:Rounding the SIGNIFICANT digits in a double, not to decimal places [duplicate]将重要数字四舍五入,而不是小数位[重复]
【发布时间】:2023-12-22 02:05:02
【问题描述】:

我需要对双精度数的有效数字进行舍入。例子 Round(1.2E-20, 0) 应该变成 1.0E-20

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

当然,我可以这样做:

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

这确实有效。但这不是:

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

在这种情况下,d 为 0.00000000013000000000000002。问题是 double 在内部存储 2 的小数,它不能完全匹配 10 的小数。在第一种情况下,C# 似乎只处理 * 和 / 的指数,但在第二种情况下,它会生成一个实际的 * 或/操作,然后会导致问题。

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

意思是我不应该在舍入后使用任何双精度运算,因为双精度运算不能完全处理小数。

上面计算的另一个问题是没有返回双精度指数的双精度函数。当然可以使用 Math 库来计算它,但可能很难保证它总是与 double 内部代码完全相同。

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

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

PS:在声明这是一个重复的问题之前,请确保您了解重要数字和小数位之间的区别

【问题讨论】:

  • “2 的分数”是指“2 的因数”吗?
  • 不要使用double,不如使用Decimal
  • 十进制显着慢,但更准确。还取决于您需要的值的范围。
  • 那些将此问题标记为重复的人,能否请他们提供指向他们认为相同的问题的链接。我在*上经历了数百个四舍五入的问题。我找不到有人问我的问题,我很乐意获得链接。
  • 我不能使用小数,因为我用双精度写了一个图表。它涵盖了整个双范围。它还支持缩放和滚动,如果舍入错误,这会导致标签和网格线出现各种可见的问题。

标签: c# double rounding


【解决方案1】:

问题是 double 在内部存储 2 的小数部分,不能完全匹配 10 的小数部分

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

请注意,对于许多计算任务(例如,但不是货币),double 类型很好。与使用 double 时存在的任何其他舍入错误相比,您没有准确获得您正在寻找的值这一事实没有更多的问题。

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

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

这将显示字符串1.3E-10;

上面计算的另一个问题是没有返回双精度指数的双精度函数

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

可能很难保证这总是与双重内部代码完全相同的结果

由于 double 的内部存储使用 IEEE 二进制 格式,其中指数和尾数都存储为二进制数,因此显示的以 10 为底的指数永远不会“与双内部代码”无论如何。当然,指数是一个整数,可以精确地表示。但这并不是一开始就存储十进制值。

无论如何,Math.Log10() 总是会返回一个有用的值。

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

如果您只是为了显示而需要舍入,则根本不要进行任何数学运算。只需使用自定义数字格式字符串(如上所述)以您想要的方式格式化值。

如果您确实需要自己进行四舍五入,那么根据您的描述,我认为以下方法应该有效:

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

【讨论】:

  • 这与我自己使用的代码几乎相同。但是对于使用 Math.Pow 的双精度并乘以或除以 exp 通常会导致低位数字出现垃圾,因此您仍然需要将数字格式化为所需的有效位数。
  • @kjbartel:“所以你仍然需要将数字格式化为所需的有效位数”——是的,完全正确。这就是为什么我建议使用 decimal 如果那个“垃圾”实际上是一个问题(例如,由于某种原因输出没有被格式化)。使用double 不可能在所有情况下都完成精确的舍入(就像在使用double 的任何其他情况下不可能总是有精确的十进制值一样)。
  • 感谢您冗长而详细的回答。不幸的是,您的最终建议与我发布的建议相同,但存在相同的问题,即在舍入后使用双精度执行任何算术运算都会破坏舍入。
  • 实际上我的代码示例与您的不同,因为它实际上适用于任意输入(即计算指数)。无论如何,如果你坚持使用double 类型,你描述的“问题”是不可能“解决”的。 double 类型根本不可能代表您希望它代表的值。
  • 确实,double不可能精确存储1.29E-10。但我正在寻找的是一个公式,它返回完全相同的数字(1.291E-10, 1)和 1.29E-10。 @PeterDuniho:我给你的答案加了 1 以表达我的感激之情。