Округлить двойные до x значащих цифр - PullRequest
59 голосов
/ 17 декабря 2008

Если у меня есть двойное число (234.004223) и т. Д., Я бы хотел округлить его до x значащих цифр в C #.

Пока что я могу найти только способы округления до x десятичных разрядов, но это просто снижает точность, если в числе есть 0.

Например, от 0,086 до одного десятичного знака становится 0,1, но я бы хотел, чтобы оно оставалось на 0,08.

Ответы [ 12 ]

76 голосов
/ 17 декабря 2008

Фреймворк не имеет встроенной функции для округления (или усечения, как в вашем примере) до ряда значащих цифр. Однако, один из способов сделать это - это масштабировать свое число так, чтобы первая значащая цифра была сразу после десятичной точки, округлить (или усечь), а затем уменьшить. Следующий код должен сделать трюк:

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);
}
21 голосов
/ 18 декабря 2009

Я уже несколько месяцев использую функцию sigfig в pDaddy и обнаружил в ней ошибку Вы не можете взять лог отрицательного числа, поэтому, если 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));
}
17 голосов
/ 17 декабря 2008

Мне кажется, что вы вообще не хотите округлять до x десятичных разрядов - вы хотите округлить до x значащих цифр. Итак, в вашем примере вы хотите округлить 0,086 до одной значащей цифры, а не до одного десятичного знака.

Теперь использование двойного и округления до числа значащих цифр проблематично для начала из-за способа хранения двойных чисел. Например, вы можете округлить 0,12 до , закрыть до 0,1, но 0,1 точно не представляется в виде двойного числа. Вы уверены, что не должны использовать десятичную дробь? Или это на самом деле для демонстрации? Если это только для целей отображения, я подозреваю, что на самом деле вы должны преобразовать double напрямую в строку с соответствующим количеством значащих цифр.

Если вы можете ответить на эти вопросы, я могу попытаться придумать подходящий код. Как это ни ужасно звучит, преобразование в число значащих цифр в виде строки путем преобразования числа в «полную» строку, а затем нахождение первой значащей цифры (а затем выполнение соответствующего действия округления после этого) может быть лучшим способом .

13 голосов
/ 07 ноября 2012

Если это для целей отображения (как вы указали в комментарии к ответу Джона Скита), вы должны использовать 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
6 голосов
/ 07 сентября 2011

Я нашел две ошибки в методах P Daddy и Eric. Это решает, например, ошибку точности, которая была представлена ​​Эндрю Хэнкоксом в этом вопросе и ответах. Была также проблема с круглыми направлениями. 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);
    }
  }
}
4 голосов
/ 20 августа 2014

Как упоминает Джон Скит: лучше обрабатывать это в текстовой области. Как правило: для целей отображения не пытайтесь округлять / изменять значения с плавающей запятой, это никогда не работает на 100%. Отображение является второстепенной задачей, и вы должны учитывать любые особые требования к форматированию, например, при работе со строками.

Мое решение, представленное ниже, было реализовано несколько лет назад и оказалось очень надежным. Это было тщательно проверено, и это работает довольно хорошо также. Примерно в 5 раз больше времени выполнения, чем решение P Daddy / Eric.

Примеры ввода + вывода приведены ниже в коде.

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;
        }
    }
}
2 голосов
/ 14 декабря 2012

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));
        }
    }
}
1 голос
/ 18 декабря 2008

Пусть 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 голос
/ 17 декабря 2008

Этот вопрос похож на тот, который вы задаете:

Форматирование чисел со значимыми цифрами в C #

Таким образом, вы можете сделать следующее:

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

Округлено до 1 значащей цифры.

0 голосов
/ 28 мая 2019

Я согласен с духом Оценка Джона :

Как бы ужасно это ни звучало, преобразование в число значащих цифр в виде строки путем преобразования числа в «полную» строку, а затем нахождение первой значащей цифры (а затем выполнение соответствующего действия округления после этого) может быть лучшим путь.

Мне потребовалось округление значащих цифр для приблизительных и не критичных к производительности вычислительных целей, и круговой анализ формата в формате "G" достаточно хорош:

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