web-dev-qa-db-fra.com

Spécificateur de largeur Printf pour conserver la précision de la valeur en virgule flottante

Existe-t-il un spécificateur printf width qui puisse être appliqué à un spécificateur à virgule flottante qui formaterait automatiquement la sortie au nombre nécessaire de chiffres significatifs, de sorte que lors du balayage de la chaîne, la valeur à virgule flottante d'origine soit acquise?

Par exemple, supposons que j'imprime une float avec une précision de 2 décimales:

float foobar = 0.9375;
printf("%.2f", foobar);    // prints out 0.94

Lorsque je balaye le 0.94 de sortie, je n'ai aucune garantie conforme aux normes que je récupèrerai la valeur virgule originale 0.9375 (dans cet exemple, je ne le ferai probablement pas).

Je voudrais pouvoir indiquer à printf d’imprimer automatiquement la valeur à virgule flottante sur le nombre nécessaire de chiffres significatifs pour qu’elle puisse être analysée à la valeur originale transmise à printf.

Je pourrais utiliser certaines des macros de float.h pour déduire la largeur maximale à passer à printf, mais existe-t-il déjà un spécificateur permettant d’imprimer automatiquement au nombre nécessaire de chiffres significatifs - ou du moins au largeur maximale?

77
Vilhelm Gray

Je recommande la solution hexadécimale @Jens Gustedt: utilisez% a.

OP veut «imprimer avec un maximum de précision (ou au moins jusqu'à la décimale la plus significative)».

Un exemple simple consisterait à imprimer un septième comme dans:

#include <float.h>
int Digs = DECIMAL_Dig;
double OneSeventh = 1.0/7.0;
printf("%.*e\n", Digs, OneSeventh);
// 1.428571428571428492127e-01

Mais creusons plus profondément ...

Mathématiquement, la réponse est "0,142857 142857 142857 ...", mais nous utilisons des nombres en virgule flottante en précision finie . Supposons IEEE 754 binaire double précision . Le OneSeventh = 1.0/7.0 donne donc la valeur ci-dessous. Sont également montrés les nombres à virgule flottante double représentables précédents et suivants.

OneSeventh before = 0.1428571428571428 214571170656199683435261249542236328125
OneSeventh        = 0.1428571428571428 49212692681248881854116916656494140625
OneSeventh after  = 0.1428571428571428 769682682968777953647077083587646484375

L'impression de la représentation décimale exacte d'une double a des utilisations limitées.

C a 2 familles de macros dans <float.h> pour nous aider.
Le premier ensemble est le nombre de chiffres significatifs à imprimer dans une chaîne en décimal. Ainsi, lors de la numérisation de la chaîne, , Nous obtenons le point flottant d’origine. Ils sont montrés avec la valeur minimum de la spécification C et un compilateur C11 sample.

FLT_DECIMAL_Dig   6,  9 (float)                           (C11)
DBL_DECIMAL_Dig  10, 17 (double)                          (C11)
LDBL_DECIMAL_Dig 10, 21 (long double)                     (C11)
DECIMAL_Dig      10, 21 (widest supported floating type)  (C99)

Le deuxième ensemble est le nombre de chiffres significatifs qu'une chaîne peut être scannée en une virgule flottante puis imprimée FP, tout en conservant la même présentation de chaîne. Ils sont montrés avec la valeur minimum de la spécification C et un compilateur C11 sample. Je crois disponible pré-C99.

FLT_Dig   6, 6 (float)
DBL_Dig  10, 15 (double)
LDBL_Dig 10, 18 (long double)

Le premier ensemble de macros semble atteindre l'objectif de chiffres significatifs d'OP. Mais cette macro n'est pas toujours disponible.

#ifdef DBL_DECIMAL_Dig
  #define OP_DBL_Digs (DBL_DECIMAL_Dig)
#else  
  #ifdef DECIMAL_Dig
    #define OP_DBL_Digs (DECIMAL_Dig)
  #else  
    #define OP_DBL_Digs (DBL_Dig + 3)
  #endif
#endif

Le "+ 3" était le noeud de ma réponse précédente . Il était centré sur le fait de connaître la chaîne de conversion aller-retour chaîne-FP-chaîne (ensemble de macros n ° 2 disponibles C89), comment déterminer les chiffres de la chaîne FP -FP (ensemble de macros n ° 1 disponible après C89)? En général, ajouter 3 était le résultat.

Maintenant, combien de significatifs chiffres à imprimer sont connus et pilotés via <float.h>

Pour imprimer N chiffres significatifs décimales, on peut utiliser différents formats. 

Avec "%e", le champ precision est le nombre de chiffres après le premier chiffre et le point décimal . Donc - 1 est dans l’ordre. Remarque: Ce -1 is not in the initialint Digs = DECIMAL_Dig; `

printf("%.*e\n", OP_DBL_Digs - 1, OneSeventh);
// 1.4285714285714285e-01

Avec "%f", le champ precision est le nombre de chiffres après le point décimal . Pour un nombre tel que OneSeventh/1000000.0, il faudrait OP_DBL_Digs + 6 pour afficher tous les chiffres significatifs.

printf("%.*f\n", OP_DBL_Digs    , OneSeventh);
// 0.14285714285714285
printf("%.*f\n", OP_DBL_Digs + 6, OneSeventh/1000000.0);
// 0.00000014285714285714285

Remarque: Beaucoup sont utilisés pour "%f". Cela affiche 6 chiffres après le point décimal; 6 est la valeur d'affichage par défaut, pas la précision du nombre.

69
chux

La réponse courte pour imprimer des nombres à virgule flottante sans perte (de sorte qu’ils puissent être lus Exactement au même nombre, sauf NaN et Infinity):

  • Si votre type est float: utilisez printf("%.9g", number).
  • Si votre type est double: utilisez printf("%.17g", number).

N'utilisez PAS %f, car cela spécifie uniquement le nombre de chiffres significatifs après la virgule et tronquera les petits nombres. Pour référence, les nombres magiques 9 et 17 peuvent être trouvés dans float.h qui définit FLT_DECIMAL_Dig et DBL_DECIMAL_Dig.

51
ccxvii

Si vous êtes uniquement intéressé par le bit (motif hexadécimal), vous pouvez utiliser le format %a. Cela vous garantit:

Le la précision par défaut suffit pour une représentation exacte de la valeur si une représentation exacte en base 2 existe et est suffisamment grande pour distinguer les valeurs de type double.

Je dois ajouter que cela n'est disponible que depuis le C99.

22
Jens Gustedt

Non, il n'y a pas de spécificateur de largeur printf de ce type permettant d'imprimer en virgule flottante avec une précision maximale. Laisse moi expliquer pourquoi.

La précision maximale de float et double est variable et dépend de la valeur réelle de float ou double.

Les rappels float et double sont stockés au format sign.exponent.mantissa format. Cela signifie que il y a beaucoup plus de bits utilisés pour la composante fractionnaire pour les petits nombres que pour les grands nombres.

enter image description here

Par exemple, float peut facilement distinguer 0,0 à 0,1.

float r = 0;
printf( "%.6f\n", r ) ; // 0.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // 0.100000

Mais float n'a aucune idée de la différence entre 1e27 et 1e27 + 0.1.

r = 1e27;
printf( "%.6f\n", r ) ; // 999999988484154753734934528.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // still 999999988484154753734934528.000000

En effet, toute la précision (limitée par le nombre de bits de mantisse) est utilisée pour la plus grande partie du nombre, à gauche de la décimale.

Le modificateur %.f indique simplement le nombre de valeurs décimales que vous souhaitez imprimer à partir du nombre à virgule flottante jusqu'à formatage Le fait que la précision disponible dépend de la taille du nombre va jusqu'à vous en tant que programmeur à manipuler. printf ne peut pas/ne gère pas cela pour vous.

10
bobobobo

Utilisez simplement les macros de <float.h> et le spécificateur de conversion à largeur variable (".*"):

float f = 3.14159265358979323846;
printf("%.*f\n", FLT_Dig, f);
9
user529758

Je lance une petite expérience pour vérifier que l'impression avec DBL_DECIMAL_Dig conserve effectivement la représentation binaire du nombre. Il s’est avéré que pour les compilateurs et les bibliothèques C que j’ai essayés, DBL_DECIMAL_Dig correspond bien au nombre de chiffres requis et que l’impression avec un chiffre au moins crée un problème important.

#include <float.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

union {
    short s[4];
    double d;
} u;

void
test(int digits)
{
    int i, j;
    char buff[40];
    double d2;
    int n, num_equal, bin_equal;

    srand(17);
    n = num_equal = bin_equal = 0;
    for (i = 0; i < 1000000; i++) {
        for (j = 0; j < 4; j++)
            u.s[j] = Rand();
        if (isnan(u.d))
            continue;
        n++;
        sprintf(buff, "%.*g", digits, u.d);
        sscanf(buff, "%lg", &d2);
        if (u.d == d2)
            num_equal++;
        if (memcmp(&u.d, &d2, sizeof(double)) == 0)
            bin_equal++;
    }
    printf("Tested %d values with %d digits: %d found numericaly equal, %d found binary equal\n", n, digits, num_equal, bin_equal);
}

int
main()
{
    test(DBL_DECIMAL_Dig);
    test(DBL_DECIMAL_Dig - 1);
    return 0;
}

Je lance ceci avec le compilateur C de Microsoft 19.00.24215.1 et la version 6.3.0 de gcc 20170516 (Debian 6.3.0-18 + deb9u1). L'utilisation d'un chiffre décimal en moins divise par deux le nombre de chiffres qui se comparent de manière parfaitement égale. (J'ai également vérifié que Rand(), tel qu'il est utilisé, produit environ un million de nombres différents.) Voici les résultats détaillés.

Microsoft C

 Valeurs testées 999523 avec 17 chiffres: 999523 trouvées numériquement égales, 999523 trouvées binaires égales 
 Valeurs testées 999523 avec 16 chiffres: 549780 trouvées numériquement égales, 549780 trouvées binaires égales 

GCC

 Valeurs 999492 testées à 17 chiffres: 999492 trouvées numériquement égales, 999492 trouvées binaires égales 
 Valeurs 999492 testées à 16 chiffres: 546615 trouvées numériquement égales, 546615 trouvées binaires égales 
0
Diomidis Spinellis

Dans l'un de mes commentaires sur une réponse, j'ai déploré le fait que je souhaitais depuis longtemps un moyen d'imprimer tous les chiffres significatifs d'une valeur à virgule flottante sous forme décimale, à peu près de la même manière que la question. Eh bien, je me suis finalement assis et l'ai écrit. Ce n'est pas tout à fait parfait, et c'est un code de démonstration qui affiche des informations supplémentaires, mais cela fonctionne principalement pour mes tests. S'il vous plaît, faites-moi savoir si vous (c'est-à-dire n'importe qui) souhaitez obtenir une copie de l'ensemble du programme d'emballage qui le pilote à des fins de test.

static unsigned int
ilog10(uintmax_t v);

/*
 * Note:  As presented this demo code prints a whole line including information
 * about how the form was arrived with, as well as in certain cases a couple of
 * interesting details about the number, such as the number of decimal places,
 * and possibley the magnitude of the value and the number of significant
 * digits.
 */
void
print_decimal(double d)
{
        size_t sigdig;
        int dplaces;
        double flintmax;

        /*
         * If we really want to see a plain decimal presentation with all of
         * the possible significant digits of precision for a floating point
         * number, then we must calculate the correct number of decimal places
         * to show with "%.*f" as follows.
         *
         * This is in lieu of always using either full on scientific notation
         * with "%e" (where the presentation is always in decimal format so we
         * can directly print the maximum number of significant digits
         * supported by the representation, taking into acount the one digit
         * represented by by the leading digit)
         *
         *        printf("%1.*e", DBL_DECIMAL_Dig - 1, d)
         *
         * or using the built-in human-friendly formatting with "%g" (where a
         * '*' parameter is used as the number of significant digits to print
         * and so we can just print exactly the maximum number supported by the
         * representation)
         *
         *         printf("%.*g", DBL_DECIMAL_Dig, d)
         *
         *
         * N.B.:  If we want the printed result to again survive a round-trip
         * conversion to binary and back, and to be rounded to a human-friendly
         * number, then we can only print DBL_Dig significant digits (instead
         * of the larger DBL_DECIMAL_Dig digits).
         *
         * Note:  "flintmax" here refers to the largest consecutive integer
         * that can be safely stored in a floating point variable without
         * losing precision.
         */
#ifdef PRINT_ROUND_TRIP_SAFE
# ifdef DBL_Dig
        sigdig = DBL_Dig;
# else
        sigdig = ilog10(uipow(FLT_RADIX, DBL_MANT_Dig - 1));
# endif
#else
# ifdef DBL_DECIMAL_Dig
        sigdig = DBL_DECIMAL_Dig;
# else
        sigdig = (size_t) lrint(ceil(DBL_MANT_Dig * log10((double) FLT_RADIX))) + 1;
# endif
#endif
        flintmax = pow((double) FLT_RADIX, (double) DBL_MANT_Dig); /* xxx use uipow() */
        if (d == 0.0) {
                printf("z = %.*s\n", (int) sigdig + 1, "0.000000000000000000000"); /* 21 */
        } else if (fabs(d) >= 0.1 &&
                   fabs(d) <= flintmax) {
                dplaces = (int) (sigdig - (size_t) lrint(ceil(log10(ceil(fabs(d))))));
                if (dplaces < 0) {
                        /* XXX this is likely never less than -1 */
                        /*
                         * XXX the last digit is not significant!!! XXX
                         *
                         * This should also be printed with sprintf() and edited...
                         */
                        printf("R = %.0f [%d too many significant digits!!!, zero decimal places]\n", d, abs(dplaces));
                } else if (dplaces == 0) {
                        /*
                         * The decimal fraction here is not significant and
                         * should always be zero  (XXX I've never seen this)
                         */
                        printf("R = %.0f [zero decimal places]\n", d);
                } else {
                        if (fabs(d) == 1.0) {
                                /*
                                 * This is a special case where the calculation
                                 * is off by one because log10(1.0) is 0, but
                                 * we still have the leading '1' whole digit to
                                 * count as a significant digit.
                                 */
#if 0
                                printf("ceil(1.0) = %f, log10(ceil(1.0)) = %f, ceil(log10(ceil(1.0))) = %f\n",
                                       ceil(fabs(d)), log10(ceil(fabs(d))), ceil(log10(ceil(fabs(d)))));
#endif
                                dplaces--;
                        }
                        /* this is really the "useful" range of %f */
                        printf("r = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                }
        } else {
                if (fabs(d) < 1.0) {
                        int lz;

                        lz = abs((int) lrint(floor(log10(fabs(d)))));
                        /* i.e. add # of leading zeros to the precision */
                        dplaces = (int) sigdig - 1 + lz;
                        printf("f = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                } else {                /* d > flintmax */
                        size_t n;
                        size_t i;
                        char *df;

                        /*
                         * hmmmm...  the easy way to suppress the "invalid",
                         * i.e. non-significant digits is to do a string
                         * replacement of all dgits after the first
                         * DBL_DECIMAL_Dig to convert them to zeros, and to
                         * round the least significant digit.
                         */
                        df = malloc((size_t) 1);
                        n = (size_t) snprintf(df, (size_t) 1, "%.1f", d);
                        n++;                /* for the NUL */
                        df = realloc(df, n);
                        (void) snprintf(df, n, "%.1f", d);
                        if ((n - 2) > sigdig) {
                                /*
                                 * XXX rounding the integer part here is "hard"
                                 * -- we would have to convert the digits up to
                                 * this point back into a binary format and
                                 * round that value appropriately in order to
                                 * do it correctly.
                                 */
                                if (df[sigdig] >= '5' && df[sigdig] <= '9') {
                                        if (df[sigdig - 1] == '9') {
                                                /*
                                                 * xxx fixing this is left as
                                                 * an exercise to the reader!
                                                 */
                                                printf("F = *** failed to round integer part at the least significant digit!!! ***\n");
                                                free(df);
                                                return;
                                        } else {
                                                df[sigdig - 1]++;
                                        }
                                }
                                for (i = sigdig; df[i] != '.'; i++) {
                                        df[i] = '0';
                                }
                        } else {
                                i = n - 1; /* less the NUL */
                                if (isnan(d) || isinf(d)) {
                                        sigdig = 0; /* "nan" or "inf" */
                                }
                        }
                        printf("F = %.*s. [0 decimal places, %lu digits, %lu digits significant]\n",
                               (int) i, df, (unsigned long int) i, (unsigned long int) sigdig);
                        free(df);
                }
        }

        return;
}


static unsigned int
msb(uintmax_t v)
{
        unsigned int mb = 0;

        while (v >>= 1) { /* unroll for more speed...  (see ilog2()) */
                mb++;
        }

        return mb;
}

static unsigned int
ilog10(uintmax_t v)
{
        unsigned int r;
        static unsigned long long int const PowersOf10[] =
                { 1LLU, 10LLU, 100LLU, 1000LLU, 10000LLU, 100000LLU, 1000000LLU,
                  10000000LLU, 100000000LLU, 1000000000LLU, 10000000000LLU,
                  100000000000LLU, 1000000000000LLU, 10000000000000LLU,
                  100000000000000LLU, 1000000000000000LLU, 10000000000000000LLU,
                  100000000000000000LLU, 1000000000000000000LLU,
                  10000000000000000000LLU };

        if (!v) {
                return ~0U;
        }
        /*
         * By the relationship "log10(v) = log2(v) / log2(10)", we need to
         * multiply "log2(v)" by "1 / log2(10)", which is approximately
         * 1233/4096, or (1233, followed by a right shift of 12).
         *
         * Finally, since the result is only an approximation that may be off
         * by one, the exact value is found by subtracting "v < PowersOf10[r]"
         * from the result.
         */
        r = ((msb(v) * 1233) >> 12) + 1;

        return r - (v < PowersOf10[r]);
}
0
Greg A. Woods