将双倍数换算为x有效数字

Roc*_*cco 66 c# math rounding significant-digits

如果我有一个双(234.004223)等,我想将其舍入为C#中的x位有效数字.

到目前为止,我只能找到舍入到x小数位的方法,但如果数字中有任何0,则只会删除精度.

例如,0.086到一位小数位变为0.1,但我希望它保持在0.08.

P D*_*ddy 83

该框架没有内置函数来将(或截断,如在您的示例中)舍入为多个有效数字.但是,您可以采用的一种方法是缩放数字,使您的第一个有效数字位于小数点后面,圆形(或截断),然后缩小.以下代码应该可以解决问题:

static double RoundToSignificantDigits(this double d, int digits){
    if(d == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);
    return scale * Math.Round(d / scale, digits);
}
Run Code Online (Sandbox Code Playgroud)

如果在您的示例中,您确实要截断,那么您需要:

static double TruncateToSignificantDigits(this double d, int digits){
    if(d == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1 - digits);
    return scale * Math.Truncate(d / scale);
}
Run Code Online (Sandbox Code Playgroud)

  • @leftbrainlogic:是的,确实如此:http://msdn.microsoft.com/en-us/library/75ks3aby.aspx (10认同)
  • 这些方法都不适用于负数,因为如果d <0,Math.Log10将返回Double.NaN. (4认同)
  • @PDaddy嗯,你需要检查d == 0,因为这也会导致Double.NaN - 两种方法都需要一些保护条款,例如:if(d == 0){return 0; } if(d <0){d = Math.Abs​​(d); } - 否则你最终会得到0的除法. (3认同)
  • @Fraser:嗯,为读者进行练习.顺便说一下,埃里克注意到(http://stackoverflow.com/a/1925170/36388)两年前的负面数字缺陷(不是零缺点).也许我应该修改这段代码,以便人们不再打电话给我. (3认同)
  • @PDaddy是的,请修复它。如果它是固定的,我会+1。我猜很多人错误地将高度投票的答案视为可复制粘贴。 (2认同)

Eri*_*ric 21

我一直在使用pDaddy的sigfig功能几个月,并发现了它的一个错误.您不能记录负数的对数,因此如果d为负数,则结果为NaN.

以下更正了错误:

public static double SetSigFigs(double d, int digits)
{   
    if(d == 0)
        return 0;

    decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);

    return (double) (scale * Math.Round((decimal)d / scale, digits));
}
Run Code Online (Sandbox Code Playgroud)

  • 失败(0.073699979,7)返回`0.073699979999999998` (4认同)
  • 出于某种原因,此代码不会将 50.846113537656557 准确地转换为 6 个 sigfigs,有什么想法吗? (2认同)

Jon*_*eet 18

这听起来像你根本不想舍入到x小数位 - 你想要舍入到x有效数字.因此,在您的示例中,您希望将0.086舍入为一个有效数字,而不是一个小数位.

现在,由于存储双精度的方式,使用双精度和舍入到多个有效数字是有问题的.例如,您可以将0.12舍入到接近 0.1的值,但0.1不能完全表示为double.你确定你不应该使用小数吗?或者,这实际上是用于显示目的吗?如果是出于显示目的,我怀疑你应该将double直接转换为具有相关有效位数的字符串.

如果你能回答这些问题,我可以尝试提出一些适当的代码.听起来很糟糕,通过将数字转换为"完整"字符串然后找到第一个有效数字(然后在此之后采取适当的舍入操作)转换为多个有效数字作为字符串可能是最好的方法.

  • @Rocco:我知道我迟到了 4 年,但我刚刚遇到了你的问题。我认为你应该使用 Double.ToString("Gn")。请参阅我在 2012 年 11 月 6 日的回答:-) (4认同)

far*_*ast 14

如果它是出于显示目的(正如你在对Jon Skeet的回答的评论中所述),你应该使用Gn 格式说明符.其中n是有效位数 - 正是你所追求的.

如果您想要3位有效数字(打印输出位于每行的注释中),以下是使用示例:

    Console.WriteLine(1.2345e-10.ToString("G3"));//1.23E-10
    Console.WriteLine(1.2345e-5.ToString("G3")); //1.23E-05
    Console.WriteLine(1.2345e-4.ToString("G3")); //0.000123
    Console.WriteLine(1.2345e-3.ToString("G3")); //0.00123
    Console.WriteLine(1.2345e-2.ToString("G3")); //0.0123
    Console.WriteLine(1.2345e-1.ToString("G3")); //0.123
    Console.WriteLine(1.2345e2.ToString("G3"));  //123
    Console.WriteLine(1.2345e3.ToString("G3"));  //1.23E+03
    Console.WriteLine(1.2345e4.ToString("G3"));  //1.23E+04
    Console.WriteLine(1.2345e5.ToString("G3"));  //1.23E+05
    Console.WriteLine(1.2345e10.ToString("G3")); //1.23E+10
Run Code Online (Sandbox Code Playgroud)

  • 虽然接近,但这并不总是返回sigfigs ...例如,`G4`会从`1.000` - >`1`中删除零.此外,无论你喜不喜欢,它都会自行决定科学记谱法. (4认同)
  • 应该同意你在 1.0001 中删除重要的零。至于第二个陈述——科学记数法的使用是根据哪个记数法在打印时占用更少空间的事实来决定的(这是 G 格式的旧 FORTRAN 规则)。所以,在某种程度上它是可以预测的,但是如果有人通常更喜欢科学格式 - 这对他们来说并不好。 (2认同)

小智 6

我在P爸爸和埃里克的方法中发现了两个错误.这解决了例如Andrew Hancox在本问答中提出的精度误差.圆方向也存在问题.有两个有效数字的1050不是1000.0,而是1100.0.使用MidpointRounding.AwayFromZero修正了舍入.

static void Main(string[] args) {
  double x = RoundToSignificantDigits(1050, 2); // Old = 1000.0, New = 1100.0
  double y = RoundToSignificantDigits(5084611353.0, 4); // Old = 5084999999.999999, New = 5085000000.0
  double z = RoundToSignificantDigits(50.846, 4); // Old = 50.849999999999994, New =  50.85
}

static double RoundToSignificantDigits(double d, int digits) {
  if (d == 0.0) {
    return 0.0;
  }
  else {
    double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(d))) + 1;
    double scale = Math.Pow(10, leftSideNumbers);
    double result = scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero);

    // Clean possible precision error.
    if ((int)leftSideNumbers >= digits) {
      return Math.Round(result, 0, MidpointRounding.AwayFromZero);
    }
    else {
      return Math.Round(result, digits - (int)leftSideNumbers, MidpointRounding.AwayFromZero);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • RoundToSignificantDigits(.0000000000000000846113537656557, 6) 失败,因为 Math.Round 不允许它的第二个参数超过 15。 (2认同)

Kay*_*Zed 5

正如 Jon Skeet 所提到的:在文本领域更好地处理这个问题。通常:出于显示目的,不要尝试舍入/更改您的浮点值,它永远不会 100% 有效。显示是次要问题,您应该处理任何特殊的格式要求,例如处理字符串。

我几年前实施的以下解决方案已被证明非常可靠。它已经过彻底的测试,而且性能也很好。执行时间比 P Daddy / Eric 的解决方案长约 5 倍。

下面在代码中给出的输入 + 输出示例。

using System;
using System.Text;

namespace KZ.SigDig
{
    public static class SignificantDigits
    {
        public static string DecimalSeparator;

        static SignificantDigits()
        {
            System.Globalization.CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentCulture;
            DecimalSeparator = ci.NumberFormat.NumberDecimalSeparator;
        }

        /// <summary>
        /// Format a double to a given number of significant digits.
        /// </summary>
        /// <example>
        /// 0.086 -> "0.09" (digits = 1)
        /// 0.00030908 -> "0.00031" (digits = 2)
        /// 1239451.0 -> "1240000" (digits = 3)
        /// 5084611353.0 -> "5085000000" (digits = 4)
        /// 0.00000000000000000846113537656557 -> "0.00000000000000000846114" (digits = 6)
        /// 50.8437 -> "50.84" (digits = 4)
        /// 50.846 -> "50.85" (digits = 4)
        /// 990.0 -> "1000" (digits = 1)
        /// -5488.0 -> "-5000" (digits = 1)
        /// -990.0 -> "-1000" (digits = 1)
        /// 0.0000789 -> "0.000079" (digits = 2)
        /// </example>
        public static string Format(double number, int digits, bool showTrailingZeros = true, bool alwaysShowDecimalSeparator = false)
        {
            if (Double.IsNaN(number) ||
                Double.IsInfinity(number))
            {
                return number.ToString();
            }

            string sSign = "";
            string sBefore = "0"; // Before the decimal separator
            string sAfter = ""; // After the decimal separator

            if (number != 0d)
            {
                if (digits < 1)
                {
                    throw new ArgumentException("The digits parameter must be greater than zero.");
                }

                if (number < 0d)
                {
                    sSign = "-";
                    number = Math.Abs(number);
                }

                // Use scientific formatting as an intermediate step
                string sFormatString = "{0:" + new String('#', digits) + "E0}";
                string sScientific = String.Format(sFormatString, number);

                string sSignificand = sScientific.Substring(0, digits);
                int exponent = Int32.Parse(sScientific.Substring(digits + 1));
                // (the significand now already contains the requested number of digits with no decimal separator in it)

                StringBuilder sFractionalBreakup = new StringBuilder(sSignificand);

                if (!showTrailingZeros)
                {
                    while (sFractionalBreakup[sFractionalBreakup.Length - 1] == '0')
                    {
                        sFractionalBreakup.Length--;
                        exponent++;
                    }
                }

                // Place decimal separator (insert zeros if necessary)

                int separatorPosition = 0;

                if ((sFractionalBreakup.Length + exponent) < 1)
                {
                    sFractionalBreakup.Insert(0, "0", 1 - sFractionalBreakup.Length - exponent);
                    separatorPosition = 1;
                }
                else if (exponent > 0)
                {
                    sFractionalBreakup.Append('0', exponent);
                    separatorPosition = sFractionalBreakup.Length;
                }
                else
                {
                    separatorPosition = sFractionalBreakup.Length + exponent;
                }

                sBefore = sFractionalBreakup.ToString();

                if (separatorPosition < sBefore.Length)
                {
                    sAfter = sBefore.Substring(separatorPosition);
                    sBefore = sBefore.Remove(separatorPosition);
                }
            }

            string sReturnValue = sSign + sBefore;

            if (sAfter == "")
            {
                if (alwaysShowDecimalSeparator)
                {
                    sReturnValue += DecimalSeparator + "0";
                }
            }
            else
            {
                sReturnValue += DecimalSeparator + sAfter;
            }

            return sReturnValue;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Wil*_*iam 5

我同意Jon 的评价精神:

听起来很糟糕,通过将数字转换为“完整”字符串,然后找到第一个有效数字(然后在此之后采取适当的舍入操作),将数字转换为字符串,这可能是最好的方法。

我需要对近似非性能关键的计算目的进行有效数字舍入,并且通过“G”格式的格式解析往返就足够了:

public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits)
{
    return double.Parse(value.ToString("G" + numberOfSignificantDigits));
}
Run Code Online (Sandbox Code Playgroud)