Autour d'un double à X chiffres significatifs

si j'ai un double (234.004223), etc. Je voudrais arrondit à x chiffres significatifs en C#.

jusqu'à présent, je ne peux trouver que des moyens de arrondir à la décimale x, mais cela enlève tout 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.

51
demandé sur Peter Mortensen 2008-12-17 14:52:09

11 réponses

le framework n'a pas de fonction intégrée pour arrondir (ou tronquer, comme dans votre exemple) à un nombre de chiffres significatifs. Une façon de faire cela, cependant, est de mettre à l'échelle votre nombre de sorte que votre premier chiffre significatif est juste après le point décimal, rond (ou tronqué), puis l'échelle en arrière. Le code suivant devrait faire l'affaire:

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

Si, comme dans votre exemple, vous voulez vraiment tronquée, alors vous voulez:

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);
}
68
répondu P Daddy 2013-05-03 12:15:45

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

corrige le bogue suivant:

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));
}
20
répondu Eric 2016-12-22 18:18:23

Il me semble que vous ne voulez pas rond à x décimales à tous, vous voulez arrondir à x chiffres significatifs. Donc dans votre exemple, vous voulez arrondir 0.086 à un chiffre significatif, pas une seule décimale.

maintenant, utiliser un double et arrondir à un nombre de chiffres significatifs est problématique pour commencer, en raison de la façon dont les doubles sont stockés. Par exemple, vous pourriez arrondir 0,12 à quelque chose fermer à 0,1, mais 0,1 n'est pas exactement représentable comme un double. Tu es sûr que tu ne devrais pas utiliser une décimale? Alternativement, est-ce réellement pour des fins d'affichage? Si c'est à des fins d'affichage, je soupçonne que vous devriez convertir le double directement à 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é. Terrible comme il sonne, la conversion à un nombre de chiffres significatifs comme une corde en convertissant le nombre en une chaîne" pleine", puis la recherche du premier chiffre significatif (et ensuite la prise de mesures d'arrondi appropriées après cela) pourrait bien être la meilleure façon de procéder.

14
répondu Jon Skeet 2008-12-17 12:27:18

si c'est à des fins d'affichage (comme vous le mentionnez dans le commentaire à 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 êtes après.

Voici l'exemple d'utilisation si vous voulez 3 chiffres significatifs (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
13
répondu farfareast 2012-11-06 23:08:48

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é par Andrew Hancox dans ce Q&A. 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 la limite médiane.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);
    }
  }
}
5
répondu Rowanto 2017-05-04 21:09:16

comme le mentionne Jon Skeet: mieux vaut gérer cela dans le domaine textuel. En règle générale: pour les besoins de l'affichage, n'essayez pas d'arrondir / changer vos valeurs de virgule flottante, cela ne fonctionne jamais à 100%. L'affichage est une préoccupation secondaire et vous devriez gérer toutes les exigences spéciales de formatage comme celles-ci travaillant avec des chaînes.

ma solution ci-dessous a été implémentée il y a plusieurs années et s'est avérée très fiable. Il a été testé et il fonctionne très bien aussi. Environ 5 temps d'exécution plus long que la solution de P Daddy / Eric.

exemples d'entrées et de sorties donnés ci-dessous en 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;
        }
    }
}
3
répondu Kay Zed 2015-06-26 03:58:56
"151910920 des Mathématiques".Round () sur les doubles est défectueux (voir les Notes aux appelants dans sa documentation ). L'étape ultérieure de la multiplication du nombre arrondi de nouveau par son exposant décimal introduira d'autres erreurs de virgule flottante dans les chiffres de queue. Utiliser un autre Round() comme @Rowanto ne va pas aider de manière fiable et souffre d'autres problèmes. Cependant si vous êtes prêt à aller par décimale puis les mathématiques.Round () est fiable, tout comme multiplier et diviser 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
répondu Oliver Bock 2012-12-14 03:00:25

cette question Est Semblable à celle que vous posez:

mise en forme des nombres avec les chiffres significatifs en C#

ainsi, vous pourriez faire ce qui suit:

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

arrondi à un chiffre significatif.

1
répondu Bravax 2017-05-23 12:10:00

permet inputNumber être 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
répondu lakshmanaraj 2015-06-07 12:12:54

voici quelque chose que j'ai fait en 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;
}

J'espère que je n'ai rien changé au formatage.

-4
répondu user1275860 2017-05-04 21:14:45

j'ai juste fait:

int integer1 = Math.Round(double you want to round, 
    significant figures you want to round to)
-5
répondu Javier 2012-11-23 06:26:50