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

Posted

技术标签:

【中文标题】将双精度数舍入到 x 有效数字【英文标题】:Round a double to x significant figures 【发布时间】:2008-12-17 11:52:09 【问题描述】:

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

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

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

【问题讨论】:

您是否希望在小数点的初始“0”之后有 x 个数字。例如,如果您想将 2 位数字保留为以下数字 0.00030908 是 0.00031 还是您想要 0.00030?还是别的什么? 我不清楚你在这里的意思。在您的示例中,您是否尝试四舍五入到小数点后 2 位?还是只留下一位数字?如果是后者,它应该是 0.09,当然,四舍五入 6... 或者你在寻找 N * 10^X,其中 N 有指定的位数? 请给我们更多原始数字的示例以及您希望看到的输出结果 我不同意。舍入到有效数字并不意味着您应该自动截断而不是舍入。例如,请参阅en.wikipedia.org/wiki/Significant_figures。 “...如果将 0.039 四舍五入为 1 位有效数字,则结果将为 0.04。” 【参考方案1】:

该框架没有内置函数来舍入(或截断,如您的示例)到多个有效数字。但是,您可以做到这一点的一种方法是缩放您的数字,以便您的第一个有效数字正好在小数点之后,四舍五入(或截断),然后缩小。以下代码应该可以解决问题:

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

【讨论】:

@leftbrainlogic:是的,确实如此:msdn.microsoft.com/en-us/library/75ks3aby.aspx 这两种方法都不适用于负数,因为如果 d @PDaddy 嗯,你需要检查是否 d == 0 因为这也会导致 Double.NaN - 这两种方法都需要几个保护子句,例如: if(d == 0) 返回 0; if(d @Fraser:好吧,读者可以练习了。顺便说一句,Eric 注意到 (***.com/a/1925170/36388) 两年前的负数缺陷(虽然不是零)。也许我真的应该修复这段代码,这样人们就不会再打电话给我了。 @PDaddy 是的,请修复它。如果它已修复,我会 +1。我猜很多人错误地将投票率高的答案视为可复制粘贴。【参考方案2】:

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

【讨论】:

由于某种原因,此代码无法准确地将 50.846113537656557 转换为 6 sigfigs,有什么想法吗? 失败并返回 (0.073699979, 7) 返回0.073699979999999998【参考方案3】:

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

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

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

【讨论】:

这是为了显示目的,老实说,我根本没有考虑过小数。如您所说,我将如何转换为具有相关有效位数的字符串?我一直无法在 Double.ToString() 方法规范中找到示例。 @Rocco:我知道我迟到了 4 年,但我刚刚遇到了你的问题。我认为你应该使用 Double.ToString("Gn")。请参阅我在 2012 年 11 月 6 日的回答 :-)【参考方案4】:

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

【讨论】:

虽然关闭,但这并不总是返回 sigfigs... 例如,G4 将删除 1.000 中的零 --> 1。此外,无论您喜欢与否,它都会自行决定强制使用科学记数法。 应该同意你在 1.0001 中删除重要的零。至于第二个陈述——科学记数法的使用是基于这样一个事实决定的,即哪种记数法在打印上占用的空间更少(这是 G 格式的旧 FORTRAN 规则)。所以,在某种程度上它是可以预测的,但如果有人通常更喜欢科学格式 - 这对他们来说并不好。 这绝对是解决我问题的最佳方案。我向 API 提交了精度为 28 位的 30/31,API 通过返回一个与我的原始值不匹配的 16 位值来确认它。为了匹配这些值,我现在将submittedValue.ToString("G12")returnedValue.ToString("G12") 进行比较(在我的情况下这已经足够精确了)。【参考方案5】:

我在 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);
    
  

【讨论】:

RoundToSignificantDigits(.00000000000000000846113537656557, 6) 失败,因为 Math.Round 不允许其第二个参数超过 15。 我认为,1050 舍入到两位有效数字就是 1000。舍入到偶数是一种非常常见的舍入方法。【参考方案6】:

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

【讨论】:

【参考方案7】:

双打上的Math.Round() 是有缺陷的(请参阅documentation 中的来电者注意事项)。将四舍五入的数字乘以其十进制指数的后续步骤将在尾随数字中引入进一步的浮点错误。像@Rowanto 那样使用另一个 Round() 不会可靠地提供帮助,并且会遇到其他问题。但是,如果您愿意使用小数,那么 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));
        
    

【讨论】:

【参考方案8】:

我同意Jon's assessment的精神:

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

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

public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits)

    return double.Parse(value.ToString("G" + numberOfSignificantDigits));

【讨论】:

【参考方案9】:

这个问题和你问的类似:

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 位有效数字。

【讨论】:

返回 2340.0004 - 至少有一些本地化。【参考方案10】:

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


【讨论】:

【参考方案11】:

正如@Oliver Bock 所指出的那样,双打上的 Math.Round() 是有缺陷的(请参阅 documentation 中的来电者注意事项)。将四舍五入的数字乘以其十进制指数的后续步骤将在尾随数字中引入进一步的浮点错误。通常,任何乘以或除以 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】:

对我来说,这个很好用,也适用于负数:

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

【讨论】:

【参考方案13】:

我的解决方案在某些情况下可能会有所帮助,我用它来显示大小差异很大的加密货币价格 - 它总是给我指定数量的有效数字,但与 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($"FdecPlaces");

【讨论】:

【参考方案14】:

我刚刚做了:

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

【讨论】:

这只会为您提供小数点右侧的有效位数。【参考方案15】:

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

希望我没有更改任何格式。

【讨论】:

问题标记为 C#

以上是关于将双精度数舍入到 x 有效数字的主要内容,如果未能解决你的问题,请参考以下文章

如何在小数点后将 Dart 中的双精度数舍入到给定的精度?

将双精度舍入到最接近的非次正规表示

如何优化 spark 函数以将双精度值舍入到小数点后 2 位?

在java中将双精度值舍入为两位有效数字[重复]

将双精度值舍入为 2 位十进制数字 [重复]

R:将 p 值舍入到 xtable 中的两位有效数字,并将 ReporteRs 导出到 Latex/Office/LibreOffice