簡體   English   中英

將雙精度數舍入到 x 有效數字

[英]Round a double to x significant figures

如果我有一個雙精度數(234.004223)等,我想將它四舍五入到 C# 中的 x 個有效數字。

到目前為止,我只能找到四舍五入到 x 位小數的方法,但是如果數字中有任何 0,這只會刪除精度。

例如,0.086 到小數點后一位變成 0.1,但我希望它保持在 0.08。

該框架沒有內置函數來四舍五入(或截斷,如您的示例)到多個有效數字。 但是,您可以做到這一點的一種方法是縮放您的數字,以便您的第一個有效數字正好在小數點之后,四舍五入(或截斷),然后縮小。 以下代碼應該可以解決問題:

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

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

在我看來,您根本不想四舍五入到 x 位小數 - 您想要四舍五入到 x 個有效數字。 因此,在您的示例中,您希望將 0.086 舍入到一位有效數字,而不是一位小數位。

現在,由於雙精度數的存儲方式,使用雙精度數並舍入到一些有效數字開始是有問題的。 例如,您可以將 0.12 舍入到接近0.1 的值,但 0.1 不能完全表示為雙精度數。 你確定你實際上不應該使用小數嗎? 或者,這實際上是出於展示目的嗎? 如果是出於顯示目的,我懷疑您實際上應該將雙精度直接轉換為具有相關有效位數的字符串。

如果你能回答這些問題,我可以試着想出一些合適的代碼。 聽起來很糟糕,通過將數字轉換為“完整”字符串然后找到第一個有效數字(然后采取適當的舍入操作)將數字轉換為字符串作為字符串可能是最好的方法.

如果是出於顯示目的(正如您在對 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

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

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

雙打上的 Math.Round() 是有缺陷的(請參閱其文檔中的調用者注釋)。 將四舍五入的數字乘以其十進制指數的后續步驟將在尾隨數字中引入進一步的浮點錯誤。 像@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));
        }
    }
}

我同意喬恩評估的精神:

聽起來很糟糕,通過將數字轉換為“完整”字符串然后找到第一個有效數字(然后采取適當的舍入操作)將數字轉換為字符串作為字符串可能是最好的方法.

為了近似非性能關鍵的計算目的,我需要有效數字舍入,並且通過“G”格式的格式解析往返就足夠了:

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

這個問題與您要問的問題類似:

在 C# 中使用有效數字格式化數字

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

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

四舍五入到 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

}

正如@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 的冪一樣:

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

對我來說,這個工作得很好,也適用於負數:

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

我的解決方案在某些情況下可能會有所幫助,我用它來顯示幅度差異很大的加密貨幣價格 - 它總是給我指定數量的有效數字,但與 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}");

在 .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
}

這是一個受 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);
}

我已經做了:

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

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

希望我沒有改變任何格式化它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM