Вопрос

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

Пока я могу найти способы округления только до 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);
}

Другие советы

Я использую функцию 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));
}

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

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

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

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

Я обнаружил две ошибки в методах P Daddy и Eric.Это устраняет, например, ошибку точности, которая была представлена Эндрю Хэнкоксом в этом Q & A.Была также проблема с округлыми направлениями.1050 с двумя значащими цифрами - это не 1000,0, а 1100,0.Округление было исправлено с помощью округления средней точки.Вдали от нуля.

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

Как упоминает Джон Скит:лучше разберитесь с этим в текстовой области.Как правило,:в целях отображения не пытайтесь округлять / изменять значения с плавающей запятой, это никогда не работает на 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;
        }
    }
}

Математика.Функция 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));
        }
    }
}

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

Форматирование чисел значащими цифрами в 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

}

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

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

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

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

Я только что сделал:

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 с атрибуция
Не связан с StackOverflow
scroll top