将双数取整为x个有效数字


71

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

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

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


1
您是否希望在小数点首字母“ 0”之后输入x个数字?例如,如果您想在后面的数字中保留2位数字0.00030908是0.00031,还是想要0.00030?或者是其他东西?
lakshmanaraj

1
我不清楚您在这里的意思。在您的示例中,您是否要四舍五入到小数点后两位?还是只留下一位数字?如果是后者,它应该是0.09,可以肯定,围捕6 ...
的原型保罗

还是在寻找N * 10 ^ X,其中N具有指定的位数?
原型保罗,

请给我们更多一些原始数字的示例以及您想在输出中看到的内容
The Archetypal Paul'Dec

8
我不同意。四舍五入到有效数字并不意味着您应该自动截断而不是四舍五入。例如,请参见en.wikipedia.org/wiki/Significant_figures。“ ...如果将0.039舍入为1有效数字,则结果将为0.04。”
P爸爸

Answers:


92

该框架没有内置函数来舍入(或截断,如您的示例所示)为多个有效数字。但是,您可以执行此操作的一种方法是缩放数字,以使您的第一个有效数字紧靠小数点后,四舍五入(或截断),然后缩减。以下代码可以解决问题:

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

11
@leftbrainlogic:是的,的确如此:msdn.microsoft.com/en-us/library/75ks3aby.aspx
P Daddy 2009年

5
这两种方法都不适用负数,因为如果d <0,Math.Log10将返回Double.NaN。–
Fraser

4
@PDaddy hmmm,您将需要检查d == 0是否也会产生Double.NaN-两种方法都需要几个保护子句,例如:if(d == 0){return 0; } if(d <0){d = Math.Abs​​(d); }-否则,两者都将被0除。
弗雷泽

4
@Fraser:好吧,读者可以练习一下。顺便说一句,埃里克(Eric)注意到(stackoverflow.com/a/1925170/36388)两年前的负数漏洞(尽管不是零)。也许我应该真正修复此代码,以便人们不再打电话给我。
P Daddy

3
@PDaddy是的,请修复它。如果它是固定的,我会+1。我猜很多人错误地将高度投票的答案视为可复制粘贴。
Evgeniy Berezovsky

23

我已经使用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));
}

2
由于某种原因,此代码无法将50.846113537656557准确地转换为6个sigfig,有什么想法吗?
Andrew Hancox 2010年

5
失败,返回(0.073699979,7)0.073699979999999998
LearningJrDev'Dec

20

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

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

如果您能回答这些问题,我可以尝试提出一些适当的代码。听起来很糟糕,将数字转换为“完整”字符串然后转换为第一位有效数字(然后再进行适当的舍入操作),将其转换为字符串的有效位数可能是最好的方法。


出于显示目的,老实说,我根本没有考虑过十进制。如您所说,我将如何转换为具有相关有效数字位数的字符串?我一直无法在Double.ToString()方法规范中找到示例。
罗科

4
@Rocco:我知道我迟到了4年,但我刚遇到您的问题。我认为您应该使用Double.ToString(“ Gn”)。请参阅我的2012年11月6日的回答:-)
远东地区

17

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

5
尽管关闭,但并不总是返回sigfigs ...例如,G4将从1.000->中删除零1。此外,无论您是否喜欢,它都会根据自己的判断强制采用科学记数法。
u8it '16

2
您可能应该同意在1.0001处掉零。至于第二种说法-科学表示法的使用是基于这样一种事实来决定的:该表示法将占用较少的打印空间(这是G格式的旧FORTRAN规则)。因此,从某种意义上讲,这是可以预见的,但是如果有人普遍喜欢科学格式,那对他们来说就不好了。
farfareast

9

我在P Daddy和Eric的方法中发现了两个错误。例如,这可以解决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);
    }
  }
}

2
由于Math.Round不允许第二个参数超过15,因此RoundToSignificantDigits(.00000000000000000846113537656557,6)失败。–
Oliver Bock

我认为1050舍入到两位有效数字就是1000。舍入到偶数是一种非常常见的舍入方法。
德里克·穆勒

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

3

双打上的Math.Round()有缺陷(请参阅其文档中的“调用方注意事项” )。将舍入后的数字乘以十进制指数的后面的步骤将在尾随数字中引入更多的浮点错误。将另一个Round()用作@Rowanto不能可靠地帮助您,并且会遇到其他问题。但是,如果您愿意使用小数,则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));
        }
    }
}

2

这个问题与您要问的问题类似:

在C#中用有效数字格式化数字

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

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

舍入到1个有效数字。


返回2340.0004-至少在某些本地化版本中返回。
古斯塔夫

2

让我们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

}

2

我同意乔恩的评估精神:

听起来很糟糕,将数字转换为“完整”字符串然后转换为第一位有效数字(然后再进行适当的舍入操作),将其转换为字符串的有效位数可能是最好的方法。

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

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

0

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

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

0

正如@Oliver Bock指出的那样,双打上的Math.Round()存在缺陷(请参阅其文档中的“调用方注意事项” )。将舍入后的数字乘以十进制指数的后面的步骤将在尾随数字中引入更多的浮点错误。通常,任何乘以10或乘以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的幂也是如此:

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

0

在某些情况下,我的解决方案可能会有所帮助,我用它来显示大小不等的加密价格-它始终为我提供指定数量的有效数字,但与ToString(“ G [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}");

-6

我已经做了:

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

这样只会使您获得小数点右边的
罗伯特·哈维

-7

这是我在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;
}

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


3
现在的问题是标签C#
Sнаđошƒаӽ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.