将双精度舍入为 x 有效数字

发布于 2024-07-10 15:49:01 字数 168 浏览 14 评论 0原文

如果我有一个 double (234.004223) 等,我想在 C# 中将其四舍五入到 x 个有效数字。

到目前为止,我只能找到四舍五入到小数点后 x 位的方法,但是如果数字中有任何 0,这只会删除精度。

例如,0.086 到小数点后一位变成 0.1,但我希望它保持在 0.08。

If I have a double (234.004223), etc., I would like to round this to x significant digits in C#.

So far I can only find ways to round to x decimal places, but this simply removes the precision if there are any 0s in the number.

For example, 0.086 to one decimal place becomes 0.1, but I would like it to stay at 0.08.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(17

紫南 2024-07-17 15:49:01

该框架没有内置函数来舍入(或截断,如您的示例中)为多个有效数字。 不过,实现此目的的一种方法是缩放数字,使第一个有效数字位于小数点之后,四舍五入(或截断),然后缩小。 下面的代码应该可以解决这个问题:

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

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

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

The framework doesn't have a built-in function to round (or truncate, as in your example) to a number of significant digits. One way you can do this, though, is to scale your number so that your first significant digit is right after the decimal point, round (or truncate), then scale back. The following code should do the trick:

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

If, as in your example, you really want to truncate, then you want:

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);
}
雪花飘飘的天空 2024-07-17 15:49:01

我已经使用 pDaddy 的 sigfig 函数几个月了,发现其中有一个错误。 不能取负数的 Log,因此如果 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));
}

I've been using pDaddy's sigfig function for a few months and found a bug in it. You cannot take the Log of a negative number, so if d is negative the results is NaN.

The following corrects the bug:

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));
}
剑心龙吟 2024-07-17 15:49:01

在我看来,您根本不想四舍五入到 x 个小数位 - 您想四舍五入到 x 个有效数字。 因此,在您的示例中,您希望将 0.086 四舍五入到一位有效数字,而不是一位小数。

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

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

It sounds to me like you don't want to round to x decimal places at all - you want to round to x significant digits. So in your example, you want to round 0.086 to one significant digit, not one decimal place.

Now, using a double and rounding to a number of significant digits is problematic to start with, due to the way doubles are stored. For instance, you could round 0.12 to something close to 0.1, but 0.1 isn't exactly representable as a double. Are you sure you shouldn't actually be using a decimal? Alternatively, is this actually for display purposes? If it's for display purposes, I suspect you should actually convert the double directly to a string with the relevant number of significant digits.

If you can answer those points, I can try to come up with some appropriate code. Awful as it sounds, converting to a number of significant digits as a string by converting the number to a "full" string and then finding the first significant digit (and then taking appropriate rounding action after that) may well be the best way to go.

太阳哥哥 2024-07-17 15:49:01

如果是出于显示目的(正如您在 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

If it is for display purposes (as you state in the comment to Jon Skeet's answer), you should use Gn format specifier. Where n is the number of significant digits - exactly what you are after.

Here is the the example of usage if you want 3 significant digits (printed output is in the comment of each line):

    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
许你一世情深 2024-07-17 15:49:01

我发现P爸爸和Eric的方法有两个bug。 例如,这解决了 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);
    }
  }
}

I found two bugs in the methods of P Daddy and Eric. This solves for example the precision error that was presented by Andrew Hancox in this Q&A. There was also a problem with round directions. 1050 with two significant figures isn't 1000.0, it's 1100.0. The rounding was fixed with 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);
    }
  }
}
思念绕指尖 2024-07-17 15:49:01

简而言之,这是一种可靠的解决方案。 它总是有效,其原因是它适用于字符串格式。 使用 LogPow 你总是会被特殊值所困扰 - 没有解决办法,它很复杂。

示例值表:

value               digits    returns
=========================================================
0.086                    1    "0.09"
1.0                      3    "1.00"
0.1                      3    "0.100"
0.00030908               2    "0.00031"
1239451                  3    "1240000"
5084611353               4    "5085000000"
8.46113537656557E-18     6    "0.00000000000000000846114"
50.8437                  4    "50.84"
50.846                   4    "50.85"
990                      1    "1000"
-5488                    1    "-5000"
-990                     1    "-1000"
7.89E-05                 2    "0.000079"
50.84611353765656        6    "50.8461"
0.073699979              7    "0.07369998"
0                        2    "0"

这是代码:

public static class SignificantDigits
{
    public static readonly 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>
    public static string Format(double value, int digits, bool showTrailingZeros = true, bool alwaysShowDecimalSeparator = false)
    {
        if (double.IsNaN(value) ||
            double.IsInfinity(value))
        {
            return value.ToString();
        }

        string sign = "";
        string before = "0"; // Before the decimal separator
        string after = ""; // After the decimal separator

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

            if (value < 0d)
            {
                sign = "-";
                value = -value;
            }

            // Custom exponential formatting using '#' will give us exactly our digits plus an exponent
            string scientific = value.ToString(new string('#', digits) + "E0");
            string significand = scientific.Substring(0, digits);
            int exponent = int.Parse(scientific.Substring(digits + 1));
            // (significand now already contains the requested number of digits with no decimal separator in it)

            var fractionalBreakup = new StringBuilder(significand);

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

            // Place the decimal separator (insert zeros if necessary)

            int separatorPosition;

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

            before = fractionalBreakup.ToString();

            if (separatorPosition < before.Length)
            {
                after = before.Substring(separatorPosition);
                before = before.Remove(separatorPosition);
            }
        }

        string result = sign + before;

        if (after == "")
        {
            if (alwaysShowDecimalSeparator)
            {
                result += DecimalSeparator + "0";
            }
        }
        else
        {
            result += DecimalSeparator + after;
        }

        return result;
    }
}

作为旁注 - 您可能对我在另一个问题下的工程符号答案感兴趣,这里

Here is a solution that is - in one word - reliable. It just always works, and the reason for this is that it works with string formatting. With Log and Pow you will always be bitten by special values - there is no working around that, it's intricate.

Table of example values:

value               digits    returns
=========================================================
0.086                    1    "0.09"
1.0                      3    "1.00"
0.1                      3    "0.100"
0.00030908               2    "0.00031"
1239451                  3    "1240000"
5084611353               4    "5085000000"
8.46113537656557E-18     6    "0.00000000000000000846114"
50.8437                  4    "50.84"
50.846                   4    "50.85"
990                      1    "1000"
-5488                    1    "-5000"
-990                     1    "-1000"
7.89E-05                 2    "0.000079"
50.84611353765656        6    "50.8461"
0.073699979              7    "0.07369998"
0                        2    "0"

Here is the code:

public static class SignificantDigits
{
    public static readonly 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>
    public static string Format(double value, int digits, bool showTrailingZeros = true, bool alwaysShowDecimalSeparator = false)
    {
        if (double.IsNaN(value) ||
            double.IsInfinity(value))
        {
            return value.ToString();
        }

        string sign = "";
        string before = "0"; // Before the decimal separator
        string after = ""; // After the decimal separator

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

            if (value < 0d)
            {
                sign = "-";
                value = -value;
            }

            // Custom exponential formatting using '#' will give us exactly our digits plus an exponent
            string scientific = value.ToString(new string('#', digits) + "E0");
            string significand = scientific.Substring(0, digits);
            int exponent = int.Parse(scientific.Substring(digits + 1));
            // (significand now already contains the requested number of digits with no decimal separator in it)

            var fractionalBreakup = new StringBuilder(significand);

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

            // Place the decimal separator (insert zeros if necessary)

            int separatorPosition;

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

            before = fractionalBreakup.ToString();

            if (separatorPosition < before.Length)
            {
                after = before.Substring(separatorPosition);
                before = before.Remove(separatorPosition);
            }
        }

        string result = sign + before;

        if (after == "")
        {
            if (alwaysShowDecimalSeparator)
            {
                result += DecimalSeparator + "0";
            }
        }
        else
        {
            result += DecimalSeparator + after;
        }

        return result;
    }
}

As a side note - you may be interested in my engineering notation answer under another question, here.

御弟哥哥 2024-07-17 15:49:01

我同意 Jon 的评估的精神:

听起来很糟糕,通过将数字转换为“完整”字符串,然后找到第一个有效数字(然后采取适当的舍入操作),将多个有效数字转换为字符串,这可能是最好的选择还有很长的路要走。

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

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

I agree with the spirit of Jon's assessment:

Awful as it sounds, converting to a number of significant digits as a string by converting the number to a "full" string and then finding the first significant digit (and then taking appropriate rounding action after that) may well be the best way to go.

I needed significant-digit rounding for approximate and non-performance-critical computational purposes, and the format-parse round-trip through "G" format is good enough:

public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits)
{
    return double.Parse(value.ToString("G" + numberOfSignificantDigits));
}
樱娆 2024-07-17 15:49:01

双打上的 Math.Round() 存在缺陷(请参阅其文档中的调用者注释)。 稍后将四舍五入的数字乘以其十进制指数的步骤将在尾随数字中引入进一步的浮点错误。 像 @Rowanto 那样使用另一个 Round() 不会可靠地提供帮助,并且会遇到其他问题。 但是,如果您愿意使用十进制,那么 Math.Round() 是可靠的,乘以 10 和除以 10 的幂也是可靠的:

static ClassName()
{
    powersOf10 = new decimal[28 + 1 + 28];
    powersOf10[28] = 1;
    decimal pup = 1, pdown = 1;
    for (int i = 1; i < 29; i++) {
        pup *= 10;
        powersOf10[i + 28] = pup;
        pdown /= 10;
        powersOf10[28 - i] = pdown;
    }
}

/// <summary>Powers of 10 indexed by power+28.  These are all the powers
/// of 10 that can be represented using decimal.</summary>
static decimal[] powersOf10;

static double RoundToSignificantDigits(double v, int digits)
{
    if (v == 0.0 || Double.IsNaN(v) || Double.IsInfinity(v)) {
        return v;
    } else {
        int decimal_exponent = (int)Math.Floor(Math.Log10(Math.Abs(v))) + 1;
        if (decimal_exponent < -28 + digits || decimal_exponent > 28 - digits) {
            // Decimals won't help outside their range of representation.
            // Insert flawed Double solutions here if you like.
            return v;
        } else {
            decimal d = (decimal)v;
            decimal scale = powersOf10[decimal_exponent + 28];
            return (double)(scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero));
        }
    }
}

Math.Round() on doubles is flawed (see Notes to Callers in its documentation). The later step of multiplying the rounded number back up by its decimal exponent will introduce further floating point errors in the trailing digits. Using another Round() as @Rowanto does won't reliably help and suffers from other problems. However if you're willing to go via decimal then Math.Round() is reliable, as is multiplying and dividing by powers of 10:

static ClassName()
{
    powersOf10 = new decimal[28 + 1 + 28];
    powersOf10[28] = 1;
    decimal pup = 1, pdown = 1;
    for (int i = 1; i < 29; i++) {
        pup *= 10;
        powersOf10[i + 28] = pup;
        pdown /= 10;
        powersOf10[28 - i] = pdown;
    }
}

/// <summary>Powers of 10 indexed by power+28.  These are all the powers
/// of 10 that can be represented using decimal.</summary>
static decimal[] powersOf10;

static double RoundToSignificantDigits(double v, int digits)
{
    if (v == 0.0 || Double.IsNaN(v) || Double.IsInfinity(v)) {
        return v;
    } else {
        int decimal_exponent = (int)Math.Floor(Math.Log10(Math.Abs(v))) + 1;
        if (decimal_exponent < -28 + digits || decimal_exponent > 28 - digits) {
            // Decimals won't help outside their range of representation.
            // Insert flawed Double solutions here if you like.
            return v;
        } else {
            decimal d = (decimal)v;
            decimal scale = powersOf10[decimal_exponent + 28];
            return (double)(scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero));
        }
    }
}
甜心小果奶 2024-07-17 15:49:01

这个问题与您提出的问题类似:

用显着数字格式化数字C# 中的数字

因此,您可以执行以下操作:

double Input2 = 234.004223;
string Result2 = Math.Floor(Input2) + Convert.ToDouble(String.Format("{0:G1}", Input2 - Math.Floor(Input2))).ToString("R6");

四舍五入到 1 个有效数字。

This question is similiar to the one you're asking:

Formatting numbers with significant figures in C#

Thus you could do the following:

double Input2 = 234.004223;
string Result2 = Math.Floor(Input2) + Convert.ToDouble(String.Format("{0:G1}", Input2 - Math.Floor(Input2))).ToString("R6");

Rounded to 1 significant digit.

眼眸印温柔 2024-07-17 15:49:01

inputNumber为需要在小数点后进行significantDigitsRequired转换的输入,则significantDigitsResult就是下面伪代码的答案。

integerPortion = Math.truncate(**inputNumber**)

decimalPortion = myNumber-IntegerPortion

if( decimalPortion <> 0 )
{

 significantDigitsStartFrom = Math.Ceil(-log10(decimalPortion))

 scaleRequiredForTruncation= Math.Pow(10,significantDigitsStartFrom-1+**significantDigitsRequired**)

**siginficantDigitsResult** = integerPortion + ( Math.Truncate (decimalPortion*scaleRequiredForTruncation))/scaleRequiredForTruncation

}
else
{

  **siginficantDigitsResult** = integerPortion

}

Let inputNumber be input that needs to be converted with significantDigitsRequired after decimal point, then significantDigitsResult is the answer to the following pseudo code.

integerPortion = Math.truncate(**inputNumber**)

decimalPortion = myNumber-IntegerPortion

if( decimalPortion <> 0 )
{

 significantDigitsStartFrom = Math.Ceil(-log10(decimalPortion))

 scaleRequiredForTruncation= Math.Pow(10,significantDigitsStartFrom-1+**significantDigitsRequired**)

**siginficantDigitsResult** = integerPortion + ( Math.Truncate (decimalPortion*scaleRequiredForTruncation))/scaleRequiredForTruncation

}
else
{

  **siginficantDigitsResult** = integerPortion

}
通知家属抬走 2024-07-17 15:49:01

在.NET 6.0上测试

我认为,由于框架的缺陷和浮点的误差,舍入结果不一致。 因此,使用时要小心。

decimal.Parse(doubleValue.ToString("E"), NumberStyles.Float);

例子:

using System.Diagnostics;
using System.Globalization;

List<double> doubleList = new();
doubleList.Add(    0.012345);
doubleList.Add(    0.12345 );
doubleList.Add(    1.2345  );
doubleList.Add(   12.345   );
doubleList.Add(  123.45    );
doubleList.Add( 1234.5     );
doubleList.Add(12345       );
doubleList.Add(10  );
doubleList.Add( 0  );
doubleList.Add( 1  );
doubleList.Add(-1  );
doubleList.Add( 0.1);

Debug.WriteLine("");
foreach (var item in doubleList)
{
    Debug.WriteLine(decimal.Parse(item.ToString("E2"), NumberStyles.Float));

    // 0.0123
    // 0.123
    // 1.23
    // 12.3
    // 123
    // 1230
    // 12300
    // 10.0
    // 0.00
    // 1.00
    // -1.00
    // 0.100
}

Debug.WriteLine("");
foreach (var item in doubleList)
{
    Debug.WriteLine(decimal.Parse(item.ToString("E3"), NumberStyles.Float));

    // 0.01235
    // 0.1235
    // 1.234
    // 12.35
    // 123.5
    // 1234
    // 12340
    // 10.00
    // 0.000
    // 1.000
    // -1.000
    // 0.1000
}

Tested on .NET 6.0

In my opinion, the rounded results are inconsistent due to the defects of the framework and the error of the floating point. Therefore, be careful about use.

decimal.Parse(doubleValue.ToString("E"), NumberStyles.Float);

example:

using System.Diagnostics;
using System.Globalization;

List<double> doubleList = new();
doubleList.Add(    0.012345);
doubleList.Add(    0.12345 );
doubleList.Add(    1.2345  );
doubleList.Add(   12.345   );
doubleList.Add(  123.45    );
doubleList.Add( 1234.5     );
doubleList.Add(12345       );
doubleList.Add(10  );
doubleList.Add( 0  );
doubleList.Add( 1  );
doubleList.Add(-1  );
doubleList.Add( 0.1);

Debug.WriteLine("");
foreach (var item in doubleList)
{
    Debug.WriteLine(decimal.Parse(item.ToString("E2"), NumberStyles.Float));

    // 0.0123
    // 0.123
    // 1.23
    // 12.3
    // 123
    // 1230
    // 12300
    // 10.0
    // 0.00
    // 1.00
    // -1.00
    // 0.100
}

Debug.WriteLine("");
foreach (var item in doubleList)
{
    Debug.WriteLine(decimal.Parse(item.ToString("E3"), NumberStyles.Float));

    // 0.01235
    // 0.1235
    // 1.234
    // 12.35
    // 123.5
    // 1234
    // 12340
    // 10.00
    // 0.000
    // 1.000
    // -1.000
    // 0.1000
}
爱本泡沫多脆弱 2024-07-17 15:49:01

正如 @Oliver Bock 所指出的那样,双打上的 Math.Round() 是有缺陷的(请参阅其 文档)。 稍后将四舍五入的数字乘以其十进制指数的步骤将在尾随数字中引入进一步的浮点错误。 一般来说,任何乘以或除以 10 的幂都会得到不精确的结果,因为浮点通常以二进制而不是十进制表示。

使用以下函数将避免尾随数字中的浮点错误:

static double RoundToSignificantDigits(double d, int digits)
{
    if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
    {
        return d;
    }
    // Compute shift of the decimal point.
    int shift = digits - 1 - (int)Math.Floor(Math.Log10(Math.Abs(d)));

    // Return if rounding to the same or higher precision.
    int decimalPlaces = 0;
    for (long pow = 1; Math.Floor(d * pow) != (d * pow); pow *= 10) decimalPlaces++;
    if (shift >= decimalPlaces)
        return d;

    // Round to sf-1 fractional digits of normalized mantissa x.dddd
    double scale = Math.Pow(10, Math.Abs(shift));
    return shift > 0 ?
           Math.Round(d * scale, MidpointRounding.AwayFromZero) / scale :
           Math.Round(d / scale, MidpointRounding.AwayFromZero) * scale;
}

但是,如果您愿意使用小数,那么 Math.Round() 是可靠的,乘以 10 和除以 10 的幂也是可靠的:

static double RoundToSignificantDigits(double d, int digits)
{
    if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
    {
        return d;
    }
    decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);
    return (double)(scale * Math.Round((decimal)d / scale, digits, MidpointRounding.AwayFromZero));
}

Console.WriteLine("{0:G17}", RoundToSignificantDigits(5.015 * 100, 15)); // 501.5

As pointed out by @Oliver Bock is that Math.Round() on doubles is flawed (see Notes to Callers in its documentation). The later step of multiplying the rounded number back up by its decimal exponent will introduce further floating point errors in the trailing digits. Generally, any multiplication by or division by a power of ten gives a non-exact result, since floating-point is typically represented in binary, not in decimal.

Using the following function will avoid floating point errors in the trailing digits:

static double RoundToSignificantDigits(double d, int digits)
{
    if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
    {
        return d;
    }
    // Compute shift of the decimal point.
    int shift = digits - 1 - (int)Math.Floor(Math.Log10(Math.Abs(d)));

    // Return if rounding to the same or higher precision.
    int decimalPlaces = 0;
    for (long pow = 1; Math.Floor(d * pow) != (d * pow); pow *= 10) decimalPlaces++;
    if (shift >= decimalPlaces)
        return d;

    // Round to sf-1 fractional digits of normalized mantissa x.dddd
    double scale = Math.Pow(10, Math.Abs(shift));
    return shift > 0 ?
           Math.Round(d * scale, MidpointRounding.AwayFromZero) / scale :
           Math.Round(d / scale, MidpointRounding.AwayFromZero) * scale;
}

However if you're willing to go via decimal then Math.Round() is reliable, as is multiplying and dividing by powers of 10:

static double RoundToSignificantDigits(double d, int digits)
{
    if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
    {
        return d;
    }
    decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);
    return (double)(scale * Math.Round((decimal)d / scale, digits, MidpointRounding.AwayFromZero));
}

Console.WriteLine("{0:G17}", RoundToSignificantDigits(5.015 * 100, 15)); // 501.5
来日方长 2024-07-17 15:49:01

对我来说,这个效果很好,并且对于负数也有效:

public static double RoundToSignificantDigits(double number, int digits)
{
    int sign = Math.Sign(number);

    if (sign < 0)
        number *= -1;

    if (number == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(number))) + 1);
    return sign * scale * Math.Round(number / scale, digits);
}

for me, this one works pretty fine and is also valid for negative numbers:

public static double RoundToSignificantDigits(double number, int digits)
{
    int sign = Math.Sign(number);

    if (sign < 0)
        number *= -1;

    if (number == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(number))) + 1);
    return sign * scale * Math.Round(number / scale, digits);
}
慵挽 2024-07-17 15:49:01

我的解决方案在某些情况下可能会有所帮助,我用它来显示大小差异很大的加密货币价格 - 它总是给我指定数量的有效数字,但与 ToString("G[位数]") 不同,它不显示科学记数法中的小值(不知道如何使用 ToString() 避免这种情况,如果有请告诉我!)

    const int MIN_SIG_FIGS = 6; //will be one more for < 0
    int numZeros = (int)Math.Floor(Math.Log10(Math.Abs(price))); //get number of zeros before first digit, will be negative for price > 0
    int decPlaces = numZeros < MIN_SIG_FIGS
                  ? MIN_SIG_FIGS - numZeros < 0 
                        ? 0 
                        : MIN_SIG_FIGS - numZeros 
                  : 0; //dec. places: set to MIN_SIG_FIGS + number of zeros, unless numZeros greater than sig figs then no decimal places
    return price.ToString($"F{decPlaces}");

My solution may be helpful in some cases, I use it to display crypto prices which vary greatly in magnitude - it always gives me a specified number of significant figures but unlike ToString("G[number of digits]") it doesn't show small values in scientific notation (don't know a way to avoid this with ToString(), if there is then please let me know!)

    const int MIN_SIG_FIGS = 6; //will be one more for < 0
    int numZeros = (int)Math.Floor(Math.Log10(Math.Abs(price))); //get number of zeros before first digit, will be negative for price > 0
    int decPlaces = numZeros < MIN_SIG_FIGS
                  ? MIN_SIG_FIGS - numZeros < 0 
                        ? 0 
                        : MIN_SIG_FIGS - numZeros 
                  : 0; //dec. places: set to MIN_SIG_FIGS + number of zeros, unless numZeros greater than sig figs then no decimal places
    return price.ToString(
quot;F{decPlaces}");
街道布景 2024-07-17 15:49:01

这是一个受 Peter Mortensen 启发的版本,它为边缘情况(例如值为 NaN、Inf 或非常小的值)添加了一些保护措施:

public static double RoundToSignificantDigits(this double value, int digits)
{
    if (double.IsNaN(value) || double.IsInfinity(value))
        return value;
    if (value == 0.0)
        return 0.0;
    double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(value))) + 1;
    int places = digits - (int)leftSideNumbers;
    if (places > 15)
        return 0.0;
    double scale = Math.Pow(10, leftSideNumbers);
    double result = scale * Math.Round(value / scale, digits, MidpointRounding.AwayFromZero);
    if (places < 0)
        places = 0;
    return Math.Round(result, places, MidpointRounding.AwayFromZero);
}

Here's a version inspired by Peter Mortensen that adds a couple of safeguards for edge cases such as value being NaN, Inf or very small:

public static double RoundToSignificantDigits(this double value, int digits)
{
    if (double.IsNaN(value) || double.IsInfinity(value))
        return value;
    if (value == 0.0)
        return 0.0;
    double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(value))) + 1;
    int places = digits - (int)leftSideNumbers;
    if (places > 15)
        return 0.0;
    double scale = Math.Pow(10, leftSideNumbers);
    double result = scale * Math.Round(value / scale, digits, MidpointRounding.AwayFromZero);
    if (places < 0)
        places = 0;
    return Math.Round(result, places, MidpointRounding.AwayFromZero);
}
神仙妹妹 2024-07-17 15:49:01

我已经做了:

int integer1 = Math.Round(double you want to round, 
    significant figures you want to round to)

I just did:

int integer1 = Math.Round(double you want to round, 
    significant figures you want to round to)
丘比特射中我 2024-07-17 15:49:01

这是我在 C++ 中所做的事情,

/*
    I had this same problem I was writing a design sheet and
    the standard values were rounded. So not to give my
    values an advantage in a later comparison I need the
    number rounded, so I wrote this bit of code.

    It will round any double to a given number of significant
    figures. But I have a limited range written into the
    subroutine. This is to save time as my numbers were not
    very large or very small. But you can easily change that
    to the full double range, but it will take more time.

    Ross Mckinstray
    [email protected]
*/

#include <iostream>
#include <fstream>
#include <string>
#include <math.h>
#include <cmath>
#include <iomanip>

#using namespace std;

double round_off(double input, int places) {
    double roundA;
    double range = pow(10, 10); // This limits the range of the rounder to 10/10^10 - 10*10^10 if you want more change range;
    for (double j = 10/range; j< 10*range;) {
        if (input >= j && input < j*10){
            double figures = pow(10, places)/10;
            roundA = roundf(input/(j/figures))*(j/figures);
        }
        j = j*10;
    }
    cout << "\n in sub after loop";
    if (input <= 10/(10*10) && input >= 10*10) {
        roundA = input;
        cout << "\nDID NOT ROUND change range";
    }
    return roundA;
}

int main() {
    double number, sig_fig;

    do {
        cout << "\nEnter number ";
        cin >> number;
        cout << "\nEnter sig_fig ";
        cin >> sig_fig;
        double output = round_off(number, sig_fig);

        cout << setprecision(10);
        cout << "\n I= " << number;
        cout << "\n r= " <<output;
        cout << "\nEnter 0 as number to exit loop";
    }
    while (number != 0);

    return 0;
}

希望我没有更改任何格式化它的内容。

Here is something I did in C++

/*
    I had this same problem I was writing a design sheet and
    the standard values were rounded. So not to give my
    values an advantage in a later comparison I need the
    number rounded, so I wrote this bit of code.

    It will round any double to a given number of significant
    figures. But I have a limited range written into the
    subroutine. This is to save time as my numbers were not
    very large or very small. But you can easily change that
    to the full double range, but it will take more time.

    Ross Mckinstray
    [email protected]
*/

#include <iostream>
#include <fstream>
#include <string>
#include <math.h>
#include <cmath>
#include <iomanip>

#using namespace std;

double round_off(double input, int places) {
    double roundA;
    double range = pow(10, 10); // This limits the range of the rounder to 10/10^10 - 10*10^10 if you want more change range;
    for (double j = 10/range; j< 10*range;) {
        if (input >= j && input < j*10){
            double figures = pow(10, places)/10;
            roundA = roundf(input/(j/figures))*(j/figures);
        }
        j = j*10;
    }
    cout << "\n in sub after loop";
    if (input <= 10/(10*10) && input >= 10*10) {
        roundA = input;
        cout << "\nDID NOT ROUND change range";
    }
    return roundA;
}

int main() {
    double number, sig_fig;

    do {
        cout << "\nEnter number ";
        cin >> number;
        cout << "\nEnter sig_fig ";
        cin >> sig_fig;
        double output = round_off(number, sig_fig);

        cout << setprecision(10);
        cout << "\n I= " << number;
        cout << "\n r= " <<output;
        cout << "\nEnter 0 as number to exit loop";
    }
    while (number != 0);

    return 0;
}

Hopefully I did not change anything formatting it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文