Search code examples
cfloating-pointprintfprecision

How to compute precision for conversion specification %.*f to maintain precision of floating-point value?


Note: this question originated from this answer.

How to compute precision for conversion specification %.*f to maintain precision of floating-point value?

Note: Here the "maintain precision" means that after the printed value is read back (for example, by strtod or by scanf) the resulted value is equal (except NaNs) to the original value (used for the printing with conversion specification %.*f).


Solution

  • To round trip double (binary floating-point) values to decimal text via "%.*f" and back to the same value requires up to DBL_DECIMAL_DIG(commonly 17) significant digits.

    DBL_DECIMAL_DIGITS
    number of decimal digits, n, such that any floating-point number with p radix b digits can be rounded to a floating-point number with n decimal digits and back again without change to the value,
    C23dr § 5.2.4.2.2 24

    Any value magnitude >= 10DBL_DECIMAL_DIG - 1 printed with "%.0f" will print at least DBL_DECIMAL_DIG digits. It is only values smaller than that may require some digits after the decimal point.

    int prec = DBL_DECIMAL_DIG - log10(fabs(x));
    if (prec < 0) {
      prec = 0;
    }
    printf("%.*f\n", prec, x);
    

    • Care is needed with int prec = DBL_DECIMAL_DIG - log10(fabs(x)) as values very close to a power-of-10 may incur computational errors that result in an off-by-one error. Better to round and potential incur a +1 precision.

    • Select double values may easily get by with fewer digits. Could try with incrementally reducing precision.

    • Infinites, NaN and zeros may needed special handing.

    • Values about -DBL_TRUE_MIN likely needs the longest string. This is about 2 /* "-0." */ - DBL_MIN_10_EXP + DBL_DECIMAL_DIG + 1 /* \0 */.


    To find an optimal minimal format precision:

    #include <float.h>
    #include <math.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    // Buffer size needed for all `double`
    #define BUF_N (3 /* "-0." */ - DBL_MIN_10_EXP + DBL_DECIMAL_DIG + 1 /* \0 */)
    
    // Untested code.  Grandparent duty calls.
    // Needs review for off-by-1 errors.
    int round_trip_precision_min(double x) {
      if (!isfinite(x) || x == 0.0) {
        return 0;
      }
      char buf[BUF_N + 10];  // 10 extra for margin
      int prec = (int) (DBL_DECIMAL_DIG - lround(log10(fabs(x))));
      if (prec < 0) {
        prec = 0;
      }
      // Try with less precision
      while (prec > 0) {
        sprintf(buf, "%.*f", prec - 1, x);
        if (atof(buf) != x) {
          break;
        }
        prec--;
      }
      return prec;
    }