web-dev-qa-db-fra.com

Autour d'un double à x chiffres significatifs

Si j'ai un double (234.004223), etc., j'aimerais l'arrondir à x chiffres significatifs en C #.

Jusqu'à présent, je ne trouve que des moyens d'arrondir à x décimales, mais cela supprime simplement la précision s'il y a des 0 dans le nombre.

Par exemple, 0,086 à une décimale devient 0,1, mais je voudrais qu'il reste à 0,08.

63
Rocco

J'utilise la fonction sigfig de pDaddy depuis quelques mois et y ai trouvé un bogue. Vous ne pouvez pas prendre le journal d'un nombre négatif, donc si d est négatif, les résultats sont NaN.

Ce qui suit corrige le bogue:

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

Il me semble que vous ne voulez pas du tout arrondir à x décimales - vous voulez arrondir à x chiffres significatifs. Donc, dans votre exemple, vous voulez arrondir 0,086 à un chiffre significatif, pas à une décimale.

Maintenant, l'utilisation d'un double et d'un arrondi à un certain nombre de chiffres significatifs est problématique pour commencer, en raison de la façon dont les doubles sont stockés. Par exemple, vous pouvez arrondir 0,12 à quelque chose fermer à 0,1, mais 0,1 n'est pas exactement représentable comme un double. Êtes-vous sûr de ne pas utiliser de décimale? Alternativement, est-ce réellement à des fins d'affichage? Si c'est à des fins d'affichage, je pense que vous devriez réellement convertir le double directement en une chaîne avec le nombre pertinent de chiffres significatifs.

Si vous pouvez répondre à ces points, je peux essayer de trouver un code approprié. Aussi horrible que cela puisse paraître, la conversion en un nombre de chiffres significatifs sous forme de chaîne en convertissant le nombre en une chaîne "complète" puis en trouvant le premier chiffre significatif (puis en effectuant une action d'arrondi appropriée après cela) pourrait bien être la meilleure façon de procéder .

17
Jon Skeet

Si c'est à des fins d'affichage (comme vous le dites dans le commentaire de la réponse de Jon Skeet), vous devez utiliser Gn spécificateur de format . Où n est le nombre de chiffres significatifs - exactement ce que vous recherchez.

Voici l'exemple d'utilisation si vous voulez 3 chiffres significatifs (la sortie imprimée est dans le commentaire de chaque ligne):

    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

J'ai trouvé deux bugs dans les méthodes de P Daddy et Eric. Cela résout par exemple l'erreur de précision qui a été présentée par Andrew Hancox dans ce Q&R. Il y avait aussi un problème avec les directions rondes. 1050 avec deux chiffres significatifs n'est pas 1000,0, c'est 1100,0. L'arrondi a été corrigé avec 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

Comme le mentionne Jon Skeet: mieux gérer cela dans le domaine textuel. En règle générale: à des fins d'affichage, n'essayez pas d'arrondir/modifier vos valeurs à virgule flottante, cela ne fonctionne jamais tout à fait à 100%. L'affichage est une préoccupation secondaire et vous devez gérer toutes les exigences de mise en forme spéciales telles que celles qui fonctionnent avec des chaînes.

Ma solution ci-dessous, que j'ai implémentée il y a plusieurs années, s'est révélée très fiable. Il a été minutieusement testé et fonctionne très bien également. Temps d'exécution environ 5 fois plus long que la solution de P Daddy/Eric.

Exemples d'entrée + sortie donnés ci-dessous dans le code.

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

Math.Round () sur les doubles est imparfait (voir Notes aux appelants dans son documentation ). L'étape ultérieure de multiplication du nombre arrondi vers le haut par son exposant décimal introduira d'autres erreurs en virgule flottante dans les chiffres de fin. Utiliser un autre Round () comme le fait @Rowanto n'aidera pas de manière fiable et souffrira d'autres problèmes. Cependant, si vous êtes prêt à passer par la décimale, Math.Round () est fiable, tout comme la multiplication et la division par des puissances 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));
        }
    }
}
2
Oliver Bock

Cette question est similaire à celle que vous posez:

Formatage des nombres avec des chiffres significatifs en C #

Vous pouvez faire ceci:

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

Arrondi à 1 chiffre significatif.

1
Bravax

Soit inputNumber l'entrée qui doit être convertie avec significantDigitsRequired après le point décimal, puis significantDigitsResult est la réponse au pseudo-code suivant.

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

Je suis d'accord avec l'esprit de évaluation de Jon :

Aussi horrible que cela puisse paraître, la conversion en un nombre de chiffres significatifs sous forme de chaîne en convertissant le nombre en une chaîne "complète" puis en trouvant le premier chiffre significatif (puis en effectuant une action d'arrondi appropriée après cela) pourrait bien être la meilleure façon de procéder .

J'avais besoin d'arrondi de chiffres significatifs pour approximatif et non critique pour les performances à des fins de calcul et l'aller-retour au format "G" est assez bon:

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