Pergunta

Se eu tiver um duplo (234.004223), etc., eu gostaria de arredondar isso para x dígitos significativos em C #.

Até agora eu só posso encontrar maneiras para arredondar para lugares x decimais, mas isso simplesmente remove a precisão se houver alguma 0s no número.

Por exemplo, 0,086 com uma casa decimal for de 0.1, mas eu gostaria que ele ficar em 0,08.

Foi útil?

Solução

O quadro não tem uma função interna para rodada (ou truncada, como no seu exemplo) para um número de dígitos significativos. Uma maneira que você pode fazer isso, porém, é a escala do número para que o seu primeiro dígito significativo é logo após o ponto decimal, rodada (ou truncar), então escalar para trás. O código a seguir deve fazer o truque:

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

Se, como no seu exemplo, você realmente deseja truncar, então você quer:

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

Outras dicas

Eu tenho usado função sigfig de pDaddy por alguns meses e encontrou um bug nele. Você não pode tomar o Log de um número negativo, por isso, se d é negativo dos resultados é NaN.

A seguir corrige o bug:

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

Parece-me que você não quer rodada para lugares x decimais em tudo - você quer para arredondar para x dígitos significativos. Assim, no seu exemplo, você quer rodada 0,086 para um dígito significativo, não uma casa decimal.

Agora, usando um casal e arredondamento para um número de dígitos significativos é problemático para começar, devido à forma como duplos são armazenados. Por exemplo, você poderia rodada 0,12 para algo próximo para 0,1, mas 0,1 não é exatamente representável como uma dupla. Tem certeza de que não deve realmente estar usando um decimal? Alternativamente, este é realmente para fins de exibição? Se é para fins de exibição, eu suspeito que você deve realmente converter o duplo diretamente para uma string com o número relevante de dígitos significativos.

Se você puder responder esses pontos, eu posso tentar chegar a algum código apropriado. Awful quanto parece, a conversão para um número de dígitos significativos como uma string ao converter o número para uma string "cheio" e, em seguida, encontrar o primeiro dígito significativo (e, em seguida, tomar medidas arredondamento apropriado depois que) pode muito bem ser o melhor caminho a percorrer .

Se é para fins de exibição (como você estado no comentário à resposta de Jon Skeet), você deve usar Gn especificador de formato . Onde n é o número de dígitos significativos - exatamente o que você está depois.

Aqui está o exemplo do uso se você quiser 3 dígitos significativos (saída impressa é no comentário de cada linha):

    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

Eu encontrei dois erros nos métodos de P Daddy e Eric. Isto resolve por exemplo, o erro de precisão que foi apresentado por Andrew Hancox neste Q & A. Há também foi um problema com as direções redondas. 1050 com dois algarismos significativos não é 1000.0, é 1100.0. O arredondamento foi fixado com 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);
    }
  }
}

Como Jon Skeet menciona: melhor lidar com isso no domínio textual. Como regra: para fins de exibição, não tente redondo / alterar seus valores de ponto flutuante, nunca bastante funciona 100%. Display é uma preocupação secundária e você deve lidar com qualquer especial requisitos como estes trabalhar com strings formatação.

A minha solução abaixo eu implementado há vários anos e tem se mostrado muito confiável. Ele foi exaustivamente testado e ele executa muito bem também. Cerca de 5 vezes mais em tempo de execução do que a solução de P Daddy / Eric.

Exemplos de entrada + de saída dada abaixo no código.

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 () em duplas é falho (ver Notas para chamadores em sua documentação). A etapa posterior da multiplicação do número de volta arredondada para cima pelo expoente decimal vai introduzir erros de ponto flutuante adicional nos algarismos à direita. Usando outro Round () como @Rowanto não será confiável ajuda e sofre de outros problemas. No entanto, se você está disposto a ir via decimal depois Math.Round () é confiável, como é multiplicar e dividir por potências de 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));
        }
    }
}

Esta questão é semelhante ao que você está perguntando:

formatar números com figuras significativas em C #

Assim, você pode fazer o seguinte:

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

arredondado para 1 dígito significativo.

Let inputNumber ser a entrada que precisa ser convertido com significantDigitsRequired depois do ponto decimal, então significantDigitsResult é a resposta para o seguinte código pseudo.

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

}

Concordo com o espírito de de Jon avaliação:

Awful quanto parece, a conversão para um número de dígitos significativos como uma string ao converter o número para uma string "cheio" e, em seguida, encontrar o primeiro dígito significativo (e, em seguida, tomar medidas arredondamento apropriado depois que) pode muito bem ser o melhor caminho a percorrer.

Eu precisava significativa dígitos arredondamento para e fins aproximados não-desempenho-críticos computacionais, eo formato de análise de ida e volta através de "G" formato é bom o suficiente :

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

Eu apenas fiz:

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

Aqui está algo que eu fiz em 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;
}

Espero não mudar nada formatá-lo.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top