web-dev-qa-db-ja.com

倍数を有効数字xに丸める

ダブル(234.004223)などがある場合、これをC#の有効数字xに丸めたいと思います。

これまでのところ、小数点以下x桁に丸める方法を見つけることしかできませんでしたが、数値に0がある場合、これは単純に精度を削除します。

たとえば、小数点以下1桁の0.086は0.1になりますが、0.08のままにしておきたいと思います。

63
Rocco

フレームワークには、有効桁数に丸める(または、例のように切り捨てる)組み込み関数がありません。ただし、これを行う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);
}
80
P Daddy

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

小数点以下x桁に丸めたくない、つまり有効桁数xに丸めたいように思えます。したがって、例では、0.086を小数点以下1桁ではなく、有効数字1桁に丸めます。

現在、doubleを使用し、有効数字の数に丸めることは、doubleが格納される方法のため、最初は問題があります。たとえば、0.12を何かに丸めることができますclose 0.1ですが、0.1をdoubleとして正確に表現することはできません。実際に小数を使用してはいけませんか?あるいは、これは実際に表示目的のためですか?表示目的の場合、実際にdoubleを関連する有効桁数の文字列に直接変換する必要があると思います。

これらの点に答えることができれば、適切なコードを考え出すことができます。恐ろしいことですが、数字を「完全な」文字列に変換し、最初の有効数字を見つけて(そしてその後適切な丸め処理を行うことによって)文字列として有効数字の数に変換するのが最善の方法かもしれません。

17
Jon Skeet

表示目的である場合(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
14
farfareast

P DaddyとEricのメソッドに2つのバグが見つかりました。これは、たとえば、このQ&AでAndrew Hancoxが提示した精度エラーを解決します。ラウンド方向に関する問題もありました。 2つの有効数字を含む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);
    }
  }
}
6
Rowanto

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;
        }
    }
}
4
Kay Zed

DoubleのMath.Round()には欠陥があります( documentation の呼び出し側への注意を参照)。丸められた数値を10進数の指数で乗算する後のステップでは、後続の桁にさらに浮動小数点エラーが発生します。 @Rowantoとして別のRound()を使用しても、確実に助けにならず、他の問題に悩まされます。ただし、10進数を使用する場合は、10の累乗で乗算および除算するのと同様に、Math.Round()は信頼できます。

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
Oliver Bock

この質問は、あなたが尋ねている質問に似ています:

C#で有効数字で数値をフォーマットする

したがって、次のことができます。

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

有効数字1桁に丸められます。

1
Bravax

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

}
1
lakshmanaraj

Jonの評価 の精神に同意します:

恐ろしいことですが、数字を「完全な」文字列に変換し、最初の有効数字を見つけて(そしてその後適切な丸め処理を行うことによって)文字列として有効数字の数に変換するのが最善の方法かもしれません。

approximateおよびnon-performance-criticalの有効数字の丸めが必要でした計算目的であり、「G」形式による形式解析ラウンドトリップで十分です。

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