简体   繁体   English

printf 宽度说明符以保持浮点值的精度

[英]Printf width specifier to maintain precision of floating-point value

Is there a printf width specifier which can be applied to a floating point specifier that would automatically format the output to the necessary number of significant digits such that when scanning the string back in, the original floating point value is acquired?是否有printf宽度说明符可以应用于浮点说明符,该说明符会自动将输出格式化为必要数量的有效数字,以便在扫描回字符串时,获取原始浮点值?

For example, suppose I print a float to a precision of 2 decimal places:例如,假设我将float打印到小数点后2的精度:

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

When I scan the output 0.94 , I have no standards-compliant guarantee that I'll get the original 0.9375 floating-point value back (in this example, I probably won't).当我扫描输出0.94 ,我没有符合标准的保证我会得到原始的0.9375浮点值(在这个例子中,我可能不会)。

I would like a way tell printf to automatically print the floating-point value to the necessary number of significant digits to ensure that it can be scanned back to the original value passed to printf .我想要一种方法告诉printf自动将浮点值打印到必要数量的有效数字,以确保它可以被扫描回传递给printf的原始值。

I could use some of the macros in float.h to derive the maximum width to pass to printf , but is there already a specifier to automatically print to the necessary number of significant digits -- or at least to the maximum width?我可以使用float.h一些宏来推导传递给printf的最大宽度,但是是否已经有一个说明符可以自动打印到必要数量的有效数字——或者至少是最大宽度?

I recommend @Jens Gustedt hexadecimal solution: use %a.我推荐@Jens Gustedt 十六进制解决方案:使用 %a。

OP wants “print with maximum precision (or at least to the most significant decimal)”. OP 想要“以最大精度(或至少到最重要的小数)打印”。

A simple example would be to print one seventh as in:一个简单的例子是打印七分之一,如下所示:

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

But let's dig deeper ...但是让我们深入挖掘......

Mathematically, the answer is "0.142857 142857 142857 ...", but we are using finite precision floating point numbers.在数学上,答案是“0.142857 142857 142857 ...”,但我们使用的是有限精度浮点数。 Let's assume IEEE 754 double-precision binary .让我们假设IEEE 754 双精度二进制. So the OneSeventh = 1.0/7.0 results in the value below.所以OneSeventh = 1.0/7.0导致下面的值。 Also shown are the preceding and following representable double floating point numbers.还显示了前后可表示的double浮点数。

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

Printing the exact decimal representation of a double has limited uses.打印double精确十进制表示的用途有限。

C has 2 families of macros in <float.h> to help us. C 在<float.h>有 2 个宏系列来帮助我们。
The first set is the number of significant digits to print in a string in decimal so when scanning the string back, we get the original floating point.第一组是十进制字符串中要打印的有效数字的数量,因此在回扫字符串时,我们得到原始浮点数。 There are shown with the C spec's minimum value and a sample C11 compiler.显示了 C 规范的最小值示例C11 编译器。

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)

The second set is the number of significant digits a string may be scanned into a floating point and then the FP printed, still retaining the same string presentation.第二组是一个字符串可能被扫描成浮点数然后打印 FP 的有效数字的数量,仍然保留相同的字符串表示。 There are shown with the C spec's minimum value and a sample C11 compiler.显示了 C 规范的最小值示例C11 编译器。 I believe available pre-C99.我相信在 C99 之前可用。

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

The first set of macros seems to meet OP's goal of significant digits.第一组宏似乎符合 OP 的有效数字目标。 But that macro is not always available.但该并不总是可用。

#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

The "+ 3" was the crux of my previous answer. “+ 3”是我之前回答的关键。 Its centered on if knowing the round-trip conversion string-FP-string (set #2 macros available C89), how would one determine the digits for FP-string-FP (set #1 macros available post C89)?它的中心是如果知道往返转换字符串-FP-string(设置#2 宏可用C89),如何确定FP-string-FP 的数字(设置#1 宏可用后C89)? In general, add 3 was the result.一般来说,结果是加 3。

Now how many significant digits to print is known and driven via <float.h> .现在要打印多少有效数字是已知的,并通过<float.h>驱动。

To print N significant decimal digits one may use various formats.要打印 N 个有效的十进制数字,可以使用各种格式。

With "%e" , the precision field is the number of digits after the lead digit and decimal point.使用"%e"精度字段是前导数字和小数点的位数。 So - 1 is in order.所以- 1是有序的。 Note: This -1 is not in the initial int Digs = DECIMAL_DIG;注意:这个-1不在初始int Digs = DECIMAL_DIG;

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

With "%f" , the precision field is the number of digits after the decimal point.对于"%f"精度字段是小数点的位数。 For a number like OneSeventh/1000000.0 , one would need OP_DBL_Digs + 6 to see all the significant digits.对于像OneSeventh/1000000.0这样的数字,需要OP_DBL_Digs + 6才能看到所有有效数字。

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

Note: Many are use to "%f" .注意:许多用于"%f" That displays 6 digits after the decimal point;显示小数点后6位; 6 is the display default, not the precision of the number. 6 是显示默认值,而不是数字的精度。

The short answer to print floating point numbers losslessly (such that they can be read back in to exactly the same number, except NaN and Infinity):无损打印浮点数的简短答案(这样它们可以被读回完全相同的数字,除了 NaN 和 Infinity):

  • If your type is float: use printf("%.9g", number) .如果您的类型是 float:使用printf("%.9g", number)
  • If your type is double: use printf("%.17g", number) .如果您的类型是 double:使用printf("%.17g", number)

Do NOT use %f , since that only specifies how many significant digits after the decimal and will truncate small numbers.不要使用%f ,因为它只指定小数点后有多少有效数字并会截断小数。 For reference, the magic numbers 9 and 17 can be found in float.h which defines FLT_DECIMAL_DIG and DBL_DECIMAL_DIG .作为参考,幻数 9 和 17 可以在定义FLT_DECIMAL_DIGDBL_DECIMAL_DIG float.h中找到。

If you are only interested in the bit (resp hex pattern) you could use the %a format.如果您只对位(resp 十六进制模式)感兴趣,您可以使用%a格式。 This guarantees you:这保证您:

The default precision suffices for an exact representation of the value if an exact representation in base 2 exists and otherwise is sufficiently large to distinguish values of type double.如果存在以 2 为底的精确表示,则默认精度足以精确表示值,否则足够大以区分 double 类型的值。

I'd have to add that this is only available since C99.我必须补充一点,这仅在 C99 之后可用。

No, there is no such printf width specifier to print floating-point with maximum precision .不,没有这样的printf 宽度说明符以最大精度打印浮点数 Let me explain why.让我解释一下原因。

The maximum precision of float and double is variable , and dependent on the actual value of the float or double . floatdouble的最大精度是可变的,取决于floatdouble实际值

Recall float and double are stored in sign.exponent.mantissa format.调用floatdoublesign.exponent.mantissa格式存储。 This means that there are many more bits used for the fractional component for small numbers than for big numbers.这意味着用于小数的小数部分比用于大数的位多得多。

在此处输入图片说明

For example, float can easily distinguish between 0.0 and 0.1.例如, float可以轻松区分 0.0 和 0.1。

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

But float has no idea of the difference between 1e27 and 1e27 + 0.1 .但是float不知道1e271e27 + 0.1之间的区别。

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

This is because all the precision (which is limited by the number of mantissa bits) is used up for the large part of the number, left of the decimal.这是因为所有精度(受尾数位数的限制)都被用于小数点左边的大部分数字。

The %.f modifier just says how many decimal values you want to print from the float number as far as formatting goes. %.f修饰符只是说明您想从浮点数打印多少个十进制值,就格式而言。 The fact that the accuracy available depends on the size of the number is up to you as the programmer to handle.可用准确性取决于数字的大小这一事实取决于您作为程序员来处理。 printf can't/doesn't handle that for you. printf不能/不为你处理。

Simply use the macros from <float.h> and the variable-width conversion specifier ( ".*" ):只需使用<float.h>的宏和可变宽度转换说明符 ( ".*" ):

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

I run a small experiment to verify that printing with DBL_DECIMAL_DIG does indeed exactly preserve the number's binary representation.我运行了一个小实验来验证使用DBL_DECIMAL_DIG打印确实完全保留了数字的二进制表示。 It turned out that for the compilers and C libraries I tried, DBL_DECIMAL_DIG is indeed the number of digits required, and printing with even one digit less creates a significant problem.事实证明,对于我尝试过的编译器和 C 库, DBL_DECIMAL_DIG确实是所需的位数,即使打印少一位数也会产生重大问题。

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

I run this with Microsoft's C compiler 19.00.24215.1 and gcc version 7.4.0 20170516 (Debian 6.3.0-18+deb9u1).我使用 Microsoft 的 C 编译器 19.00.24215.1 和 gcc 版本 7.4.0 20170516 (Debian 6.3.0-18+deb9u1) 运行它。 Using one less decimal digit halves the number of numbers that compare exactly equal.使用少一位十进制数字将完全相等的数字数量减半。 (I also verified that rand() as used indeed produces about one million different numbers.) Here are the detailed results. (我还验证了所使用的rand()确实产生了大约一百万个不同的数字。)以下是详细结果。

Microsoft C微软C

Tested 999507 values with 17 digits: 999507 found numericaly equal, 999507 found binary equal
Tested 999507 values with 16 digits: 545389 found numericaly equal, 545389 found binary equal

GCC海湾合作委员会

Tested 999485 values with 17 digits: 999485 found numericaly equal, 999485 found binary equal
Tested 999485 values with 16 digits: 545402 found numericaly equal, 545402 found binary equal

In one of my comments to an answer I lamented that I've long wanted some way to print all the significant digits in a floating point value in decimal form, in much the same way the as the question asks.在我对答案的评论之一中,我感叹我一直想要某种方式以十进制形式打印浮点值中的所有有效数字,与问题所问的方式大致相同。 Well I finally sat down and wrote it.好吧,我终于坐下来写了。 It's not quite perfect, and this is demo code that prints additional information, but it mostly works for my tests.它不是很完美,这是打印附加信息的演示代码,但它主要适用于我的测试。 Please let me know if you (ie anyone) would like a copy of the whole wrapper program which drives it for testing.如果您(即任何人)想要驱动它进行测试的整个包装程序的副本,请告诉我。

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

To my knowledge, there is a well diffused algorithm allowing to output to the necessary number of significant digits such that when scanning the string back in, the original floating point value is acquired in dtoa.c written by David Gay, which is available here on Netlib (see also the associated paper ).据我所知,有一个很好的扩散算法允许输出到必要数量的有效数字,这样在扫描字符串时,原始浮点值会在 David Gay 编写的dtoa.c获得,可在此处获得Netlib(另见相关论文)。 This code is used eg in Python, MySQL, Scilab, and many others.此代码用于例如 Python、MySQL、Scilab 和许多其他代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM