4
\$\begingroup\$

The objective was to write a locale-independent function that prints floating-point numbers (i.e. one that always uses . as a decimal point regardless of the locale) that was going to be used in a library that was meant to produce JSON. I was aiming for the following:

  • Implementation simplicity
  • Ability to be used on different sorts of streams (writing into a file, a string, etc)
  • Reasonable corrrectness

Here's the main code:

#include <inttypes.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>

typedef int (*printfn_t)(void *, const char *, ...);

#define print(...) if (printfn(stream, __VA_ARGS__) < 0) {\
    return false;\
}

static const int PRECISION = 15;

static int count_digits(double n) {
  return floor(log10(n) + 1);
}

bool print_number(
    printfn_t printfn,
    void *stream,
    double number
) {
    if (number == 0) {
        print("0");
        
        return true;
    }
    
    if (number < 0) {
        print("-");
        
        number = -number;
    }
    
    uint64_t integral = 0, decimal = 0;
    int exponent = 0;
    
    if (number > 9007199254740991) {
        exponent = count_digits(number) - 1;
        
        number = number / pow(10, exponent);
    } else if (number < 1 / pow(10, PRECISION)) {
        exponent = count_digits(number) - 1;
        
        number = number * pow(10, -exponent);
    }
    
    integral = number;
    decimal = round(fmod(number, 1) * pow(10, PRECISION));
    
    print("%" PRId64, integral);
    
    if (decimal != 0) {
        print(".");
        
        int decimal_length = count_digits(decimal);
        
        for (int i = 0; i < PRECISION - decimal_length; i++) {
            print("0");
        }
        
        print("%" PRId64, decimal);
    }
    
    if (exponent != 0) {
        print("e%i", exponent);
    }
    
    return true;
}

And here's how you use it:

int main() {
    double input = 0;
    
    scanf("%lf", &input);
    
    print_number(
        (printfn_t) fprintf,
        stdout,
        input
    );
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ If it's acceptable to support only single-threaded applications, consider avoiding all the complexity by temporarily changing LC_NUMERIC to C locale (use setlocale(LC_NUMERIC, NULL) to obtain the current locale to revert back to). \$\endgroup\$ Apr 19 at 7:16
1
\$\begingroup\$

Implementation simplicity

As code is incorrect in too many cases, simplicity assessment is moot.

Ability to be used on different sorts of streams (writing into a file, a string, etc)

Code assumes it can pass a void * like a FILE *. Reasonable, but not specified by C.

Writing to a string deserves a size parameter.

Rather than attempt to use fprintf(), sprintf(), consider providing interface functions.

Reasonable corrrectness

corrrectness --> correctness

OP is brave. Quality conversion of a string to a double is hard.
To assess, I set up a test harness.

void test(double x) {
  // Reference output
  printf("R%.*e %20a\n", DBL_DECIMAL_DIG - 1, x, x);
  // OP's output
  printf("<");
  print_number((printfn_t) fprintf, stdout, x);
  puts(">\n");
}

int main(void) {
  const double x[] = {1.0, 1.0 / 7, DBL_MIN, DBL_TRUE_MIN, DBL_MAX, INFINITY,
      NAN, -DBL_MAX, -0.0, 1.0 + DBL_EPSILON, 1.0 - DBL_EPSILON};
  size_t n = sizeof x / sizeof *x;
  for (size_t i = 0; i < n; i++) {
    test(x[i]);
  }
}

Outright discrepancies included

R4.9406564584124654e-324            0x1p-1074
<0.-9223372036854775808e-324>                          ???

Rinf                  inf
<-9223372036854775808.-9223372036854775808e2147483647> ???

Rnan                  nan
<-9223372036854775808.-9223372036854775808>            ???

R-0.0000000000000000e+00              -0x0p+0
<0>                                                    Missing sign

R9.9999999999999978e-01 0x1.ffffffffffffep-1
<0.1000000000000000>                                   Off by 10x!

Non-finite

For NAN and infinity, consider a !isfinite(x) test and then handle those special cases.

Sign

For -0.0 correctness,

//if (number < 0) {
if (signbit(number)) {
    print("-");
    ...

Precision

double deserves up to DBL_DECIMAL_DIG (e.g. 17) places of output to properly distinguish all double from other double. OP's precisions output wobbles with inconsistent significant precision output.

I adjusted OP's code to int PRECISION = DBL_DECIMAL_DIG;, then used select values like 1.0 - DBL_EPSILON (and many others) are not quite correct.

R9.9999999999999978e-01 0x1.ffffffffffffep-1
<0.99999999999999984>

As a reference, to print the exact value of a finite double. See Function to print a double - exactly

Numeric correctness.

Each *, / and some pow(10, ) inject 0.5 [ULP] or more error into the math and there is little code compensating for these accumulating errors. Without internal extended math, I see OP's approach as unable to achieve a high quality result.

Code assumes double precision < 64 bits. Reasonable, but not specified by C.


Recommendation

Test with the usual suspects as in the test harness, with values near powers of 10 and with various random values.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.