简体   繁体   English

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

[英]Round a double to x significant figures

If I have a double (234.004223), etc., I would like to round this to x significant digits in C#.如果我有一个双精度数(234.004223)等,我想将它四舍五入到 C# 中的 x 个有效数字。

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.到目前为止,我只能找到四舍五入到 x 位小数的方法,但是如果数字中有任何 0,这只会删除精度。

For example, 0.086 to one decimal place becomes 0.1, but I would like it to stay at 0.08.例如,0.086 到小数点后一位变成 0.1,但我希望它保持在 0.08。

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

I've been using pDaddy's sigfig function for a few months and found a bug in it.我已经使用 pDaddy 的 sigfig 函数几个月了,发现其中有一个错误。 You cannot take the Log of a negative number, so if d is negative the results is NaN.您不能取负数的对数,因此如果 d 为负数,则结果为 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));
}

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.在我看来,您根本不想四舍五入到 x 位小数 - 您想要四舍五入到 x 个有效数字。 So in your example, you want to round 0.086 to one significant digit, not one decimal place.因此,在您的示例中,您希望将 0.086 舍入到一位有效数字,而不是一位小数位。

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.例如,您可以将 0.12 舍入到接近0.1 的值,但 0.1 不能完全表示为双精度数。 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.听起来很糟糕,通过将数字转换为“完整”字符串然后找到第一个有效数字(然后采取适当的舍入操作)将数字转换为字符串作为字符串可能是最好的方法.

If it is for display purposes (as you state in the comment to Jon Skeet's answer), you should use Gn format specifier .如果是出于显示目的(正如您在对 Jon Skeet 的回答的评论中所述),您应该使用 Gn format specifier Where n is the number of significant digits - exactly what you are after.其中n是有效位数 - 正是您所追求的。

Here is the the example of usage if you want 3 significant digits (printed output is in the comment of each line):如果您需要 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

I found two bugs in the methods of P Daddy and Eric.我在 P Daddy 和 Eric 的方法中发现了两个错误。 This solves for example the precision error that was presented by Andrew Hancox in this Q&A.例如,这解决了 Andrew Hancox 在本问答中提出的精度误差。 There was also a problem with round directions.圆形方向也有问题。 1050 with two significant figures isn't 1000.0, it's 1100.0.具有两个有效数字的 1050 不是 1000.0,而是 1100.0。 The rounding was fixed with MidpointRounding.AwayFromZero.使用 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);
    }
  }
}

As Jon Skeet mentions: better handle this in the textual domain.正如 Jon Skeet 所提到的:在文本域中更好地处理这个问题。 As a rule: for display purposes, don't try to round / change your floating point values, it never quite works 100%.通常:出于显示目的,不要尝试舍入/更改浮点值,它永远不会 100% 有效。 Display is a secondary concern and you should handle any special formatting requirements like these working with strings.显示是次要问题,您应该处理任何特殊的格式要求,例如使用字符串。

My solution below I implemented several years ago and has proven very reliable.我几年前实施的下面的解决方案已被证明非常可靠。 It has been thoroughly tested and it performs quite well also.它已经过彻底的测试,并且性能也很好。 About 5 times longer in execution time than P Daddy / Eric's solution.执行时间比 P Daddy / Eric 的解决方案长约 5 倍。

Examples of input + output given below in code.下面在代码中给出了输入 + 输出的示例。

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

Math.Round() on doubles is flawed (see Notes to Callers in its documentation ).双打上的 Math.Round() 是有缺陷的(请参阅其文档中的调用者注释)。 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.像@Rowanto 那样使用另一个 Round() 不会可靠地提供帮助,并且会遇到其他问题。 However if you're willing to go via decimal then Math.Round() is reliable, as is multiplying and dividing by powers of 10:但是,如果您愿意使用小数,那么 Math.Round() 是可靠的,就像乘以和除以 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));
        }
    }
}

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:为了近似非性能关键的计算目的,我需要有效数字舍入,并且通过“G”格式的格式解析往返就足够了:

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

This question is similiar to the one you're asking:这个问题与您要问的问题类似:

Formatting numbers with significant figures in C# 在 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.四舍五入到 1 位有效数字。

Let inputNumber be input that needs to be converted with significantDigitsRequired after decimal point, then significantDigitsResult is the answer to the following pseudo code.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

}

As pointed out by @Oliver Bock is that Math.Round() on doubles is flawed (see Notes to Callers in its documentation ).正如@Oliver Bock 所指出的那样,双打上的 Math.Round() 是有缺陷的(请参阅其文档中的调用者注释)。 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.通常,任何乘以或除以 10 的幂都会得到不精确的结果,因为浮点通常以二进制而不是十进制表示。

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:但是,如果您愿意使用小数,那么 Math.Round() 是可靠的,就像乘以和除以 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

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

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!)我的解决方案在某些情况下可能会有所帮助,我用它来显示幅度差异很大的加密货币价格 - 它总是给我指定数量的有效数字,但与 ToString("G[number of digits]") 不同,它不显示科学记数法中的小值(不知道用 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}");

Tested on .NET 6.0在 .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
}

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:这是一个受 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);
}

I just did:我已经做了:

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

Here is something I did in C++这是我在 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
    rmckinstray01@gmail.com
*/

#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.希望我没有改变任何格式化它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM