Printf-breedtespecificatie om de precisie van de drijvende-kommawaarde te behouden

Is er een printf-breedtespecificatie die kan worden toegepast op een drijvende-kommaspecificatie die de uitvoer automatisch zou formatteren naar het benodigde aantal significante cijfers, zodat bij het scannen van de string weer in, wordt de oorspronkelijke drijvende-kommawaarde verkregen?

Stel dat ik een floatafdruk met een precisie van 2decimalen:

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

Als ik de uitvoer 0.94scan, heb ik geen garantie dat ik de originele 0.9375drijvende-kommawaarde terugkrijg (in dit voorbeeld heb ik waarschijnlijk niet).

Ik wil graag een manier om printfte vertellen om de drijvende-kommawaarde automatisch af te drukken tot het benodigde aantal significante cijfersom ervoor te zorgen dat deze terug naar het origineel kan worden gescand waarde doorgegeven aan printf.

Ik zou een aantal van de macro’s in float.hkunnen gebruiken om de maximale breedte af te leidenom door te geven aan printf, maar is er al een specificatie om automatisch af te drukken tot het benodigde aantal significante cijfers— of in ieder geval tot de maximale breedte?


Antwoord 1, autoriteit 100%

Ik raad @Jens Gustedt hexadecimale oplossing aan: gebruik %a.

OP wil “afdrukken met maximale precisie (of op zijn minst tot de meest significante decimale)”.

Een eenvoudig voorbeeld zou zijn om een zevende af te drukken zoals in:

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

Maar laten we dieper graven …

Wiskundig gezien is het antwoord “0.142857 142857 142857 …”, maar we gebruiken drijvende-kommagetallen met eindige precisie.
Laten we aannemen dat IEEE 754 dubbel-precisie binair.
Dus de OneSeventh = 1.0/7.0resulteert in de onderstaande waarde. Ook worden de voorgaande en volgende representeerbare doublegetallen met drijvende komma getoond.

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

Het afdrukken van de exactedecimale weergave van een doubleheeft beperkte toepassingen.

C heeft 2 families van macro’s in <float.h>om ons te helpen.
De eerste set is het aantal significantecijfers dat moet worden afgedrukt in een tekenreeks in decimalen, dus wanneer de tekenreeks terug wordt gescand,
we krijgen de oorspronkelijke drijvende komma. Er worden getoond met de minimumwaarde van de C spec en een sampleC11 compiler.

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)

De tweede set is het aantal significantecijfers dat een string kan worden gescand in een drijvende komma en vervolgens de FP kan afdrukken, met behoud van dezelfde stringpresentatie. Er worden getoond met de minimumwaarde van de C spec’s en een voorbeeldC11 compiler. Ik geloof dat ze beschikbaar zijn vóór C99.

FLT_DIG   6, 6 (float)
DBL_DIG  10, 15 (double)
LDBL_DIG 10, 18 (long double)

De eerste set macro’s lijkt te voldoen aan OP’s doel van aanzienlijkecijfers. Maar die macrois niet altijd beschikbaar.

#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

De “+ 3” was de kern van mijn vorige antwoord.
Het is gecentreerd op het kennen van de round-trip conversie string-FP-string (set #2 macro’s beschikbaar C89), hoe zou men de cijfers voor FP-string-FP kunnen bepalen (set #1 macro’s beschikbaar na C89)? Over het algemeen was 3 toevoegen het resultaat.

Nu is bekend hoeveel significantecijfers er moeten worden afgedrukt via <float.h>.

Om N significantedecimale cijfers af te drukken kan men verschillende formaten gebruiken.

Met "%e"is het veld precisiehet aantal cijfers nahet eerste cijfer en de komma.
Dus - 1is in orde. Opmerking: deze -1staat niet in de initiële int Digs = DECIMAL_DIG;

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

Bij "%f"is het veld precisiehet aantal cijfers nade komma.
Voor een getal als OneSeventh/1000000.0heb je OP_DBL_Digs + 6nodig om alle significantecijfers te zien.

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

Opmerking: velen worden gebruikt voor "%f". Die 6 cijfers weergeeft na het decimale punt; 6 is de standaardinstelling, niet de precisie van het getal.


2, Autoriteit 74%

Het korte antwoord op het afdrukken van drijvende puntnummers verliesloos (zodanig dat ze kunnen worden gelezen
terug naar exact hetzelfde nummer, behalve NAN en Infinity):

  • Als uw type float is: gebruik printf("%.9g", number).
  • Als uw type dubbel is: gebruik printf("%.17g", number).

GEBRUIK NIET %f, omdat dat alleen aangeeft hoeveel significante cijfers na het decimaal en de kleine nummers zullen afknippen. Ter referentie zijn de magische nummers 9 en 17 te vinden in float.hdie u definieert FLT_DECIMAL_DIGen DBL_DECIMAL_DIG.


3, Autoriteit 24%

Als u alleen geïnteresseerd bent in het bit (resphexpatroon), kunt u de %aformaat gebruiken. Dit garandeert u:

de
Standaard precisie volstaat voor een exacte weergave van de waarde als een exacte weergave in basis 2 bestaat en anders is voldoende groot om de waarden van het type dubbel te onderscheiden.

Ik zou moeten toevoegen dat dit alleen beschikbaar is sinds C99.


4, Autoriteit 11%

Gebruik eenvoudig de macro’s van <float.h>en de variabele breedte conversiespecificator (".*"):

float f = 3.14159265358979323846;
printf("%.*f\n", FLT_DIG, f);

5, Autoriteit 6%

Ik voer een klein experiment uit om te controleren of het afdrukken met DBL_DECIMAL_DIGinderdaad precies de binaire weergave van de getal behoudt. Het bleek dat voor de compilers en C-bibliotheken die ik heb geprobeerd, DBL_DECIMAL_DIGis inderdaad het aantal digitale cijfers, en afdrukken met zelfs één cijfer minder creëert een aanzienlijk probleem.

#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() << 8) ^ 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;
}

Ik voer dit met Microsoft’s C Compiler 19.00.24215.1 en GCC versie 7.4.0 20170516 (Debian 6.3.0-18 + Deb9U1). Het gebruik van een minder decimale cijfer helft het aantal getallen dat precies gelijk vergeleken. (Ik heb ook geverifieerd dat rand()zoals gebruikt inderdaad ongeveer een miljoen verschillende nummers produceert.) Hier zijn de gedetailleerde resultaten.

Microsoft C

Getest 999507-waarden met 17 cijfers: 999507 Gevonden numeriek gelijk, 999507 gevonden binair gelijke 
Geteste 999507-waarden met 16 cijfers: 545389 numeriek gelijk gevonden, 545389 binair gelijk gevonden

GCC

Geteste 999485-waarden met 17 cijfers: 999485 numeriek gelijk gevonden, 999485 binair gelijk gevonden
Geteste 999485-waarden met 16 cijfers: 545402 numeriek gelijk gevonden, 545402 binair gelijk gevonden

Antwoord 6, autoriteit 5%

In een van mijn opmerkingen op een antwoord klaagde ik dat ik al lang een manier wilde om alle significante cijfers in een drijvende-kommawaarde in decimale vorm af te drukken, op vrijwel dezelfde manier als de vraag stelt. Nou, ik ben eindelijk gaan zitten en heb het opgeschreven. Het is niet helemaal perfect, en dit is democode die aanvullende informatie afdrukt, maar het werkt meestal voor mijn tests. Laat het me weten als je (d.w.z. iemand) een kopie wilt van het hele wrapper-programma dat het aanstuurt om te testen.

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

Antwoord 7, autoriteit 2%

Voor zover ik weet, is er een goed verspreid algoritme dat het mogelijk maakt om het benodigde aantal significante cijfers uit te voeren, zodat bij het opnieuw scannen van de string de oorspronkelijke drijvende-kommawaarde wordt verkregenin dtoa.cgeschreven door Daniel Gay, dat hierbeschikbaar is op Netlib (zie ook het bijbehorende papier). Deze code wordt b.v. in Python, MySQL, Scilab en vele anderen.

Other episodes