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

时间:2008-12-17 11:52:09

标签: c# math rounding significant-digits

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

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

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

15 个答案:

答案 0 :(得分:75)

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

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

答案 1 :(得分: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));
}

答案 2 :(得分:17)

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

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

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

答案 3 :(得分:13)

如果是出于显示目的(正如您在对Jon Skeet的回答的评论中所述),您应该使用Gn format specifier。其中 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

答案 4 :(得分:6)

我在P爸爸和埃里克的方法中发现了两个错误。这解决了例如Andrew Hancox在本Q& A中提出的精度误差。圆方向也存在问题。有两个有效数字的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);
    }
  }
}

答案 5 :(得分:4)

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

答案 6 :(得分:2)

双打上的Math.Round()存在缺陷(请参阅documentation中的来电者注释)。将舍入后的数字乘以其十进制指数的后一步骤将在尾随数字中引入进一步的浮点错误。使用另一个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));
        }
    }
}

答案 7 :(得分:1)

这个问题类似于你提出的问题:

Formatting numbers with significant figures in C#

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

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

圆满1位。

答案 8 :(得分:1)

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

}

答案 9 :(得分:0)

我同意Jon's assessment的精神:

  

听起来很糟糕,将数字转换为“完整”的字符串,然后找到第一个有效数字(然后采取适当的取整操作),将其转换为字符串形式的有效数字可能是最好的要走的路。

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

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

答案 10 :(得分: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);
}

答案 11 :(得分:0)

@Oliver Bock指出,双打上的Math.Round()有缺陷(请参见其documentation中对调用者的注释)。将舍入后的数字乘以其十进制指数的后续步骤将在尾随数字中引入更多的浮点错误。通常,任何乘以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

答案 12 :(得分:0)

在某些情况下,我的解决方案可能会有所帮助,我用它来显示幅度变化很大的加密货币价格-它始终为我提供指定数量的有效数字,但与ToString(“ G [digits]”“)不同不会以科学计数法显示较小的值(不知道使用ToString()避免这种情况的方法,如果有,请让我知道!)

boolean

答案 13 :(得分:-5)

我刚刚做了:

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

答案 14 :(得分:-5)

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

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