c – Floating-point to String Conversion with Given Precision for Fractional Part

Faced with converting floating-point values from a sensor obtained to string on an embedded system to transmit over UART, I came up with the following dbl2str() to handle either float or double input. The accuracy of the last digit in the fractional part wasn’t important as the floating point-values were from a temperature sensor on an MSP432. The intent was to avoid loading stdio.h and math.h.

The double value, an adequately sized buffer and then precision for the fractional-part are parameters to the function:

/**
 *  convert double d to string with fractional part
 *  limited to prec digits. s must have adequate
 *  storage to hold the converted value.
 */
char *dbl2str (double d, char *s,  int prec);

The approach is:

  • Handle 0.0 case where integer-part is '0' and pad fractional part to prec '0's, return at that point.
  • Save sign flag (1-negative, 0-posititve), set padding variable zeros equal to prec, change sign of floating-point value to positive if negative.
  • Nul-terminate temp string and fill from end with fractional-part conversion, subtracting 1 from zeros on each iteration, and after leaving conversion loop, pad to remaining zeros.
  • Add separator '.' and continue to fill temp string with integer-part conversion.
  • if sign add '-' to front of temp string.
  • copy temp string to buffer and return pointer to buffer.

(note: the range of floating-point values is from roughly -50.0 to 200.00 so INF was not protected against, nor was exhausting of the 32-byte buffer a consideration)

The code with test case is:

#include <stdio.h>
#include <stdint.h>

#define FPMAXC 32

/**
 *  convert double d to string with fractional part
 *  limited to prec digits. s must have adequate
 *  storage to hold the converted value.
 */
char *dbl2str (double d, char *s,  int prec)
{
    if (d == 0) {                                   /* handle zero case */
        int i = 0;
        *s = '0';                                   /* single '0' for int part */
        s(1) = '.';                                 /* separator */
        for (i = 2; i < 2 + prec; i++)              /* pad fp to prec with '0' */
            s(i) = '0';
        s(i) = 0;                                   /* nul-terminate */
            
        return s;
    }

    char tmp(FPMAXC), *p = tmp + FPMAXC - 1;        /* tmp buf, ptr to end */
    int sign = d < 0 ? 1 : 0,                       /* set sign if negative */
        mult = 1;                                   /* multiplier for precision */
    unsigned zeros = prec;                          /* padding zeros for fp */
    uint64_t ip, fp;                                /* integer & fractional parts */

    if (sign)                                       /* work with positive value */
        d = -d;

    for (int i = 0; i < prec; i++)                  /* compute multiplier */
        mult *= 10;

    ip = (uint64_t)d;                               /* set integer part */
    fp = (uint64_t)((d - ip) * mult);               /* fractional part to prec */

    *p = 0;                                         /* nul-terminate tmp */

    while (fp) {                                    /* convert fractional part */
        *--p = fp % 10 + '0';
        fp /= 10;
        if (zeros)                                  /* decrement zero pad */
            zeros--;
    }
    while (zeros--)                                 /* pad reaming zeros */
        *--p = '0';
    *--p = '.';

    if (!ip)                                        /* no integer part */
        *--p = '0';
    else
        while (ip) {                                /* convert integer part */
            *--p = ip % 10 + '0';
            ip /= 10;
        }

    if (sign)                                       /* if sign, add '-' */
        *--p = '-';

    for (int i = 0;; i++, p++) {                    /* copy to s with  */
        s(i) = *p;
        if (!*p)
            break;
    }

    return s;
}

int main (void) {
    
    char buf(FPMAXC);
    double d = 123.45678;
    
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = -d;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = 0.;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = 0.12345;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = -d;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = 123.0;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
    
    d = -d;
    printf ("% 8.3lf  =>  %8sn", d, dbl2str (d, buf, 3));
}

The function does what I intended, but would like to know if there are any obvious improvements that can be made with a slight-eye on optimization.

Program Output

./bin/dbl2str
 123.457  =>   123.456
-123.457  =>  -123.456
   0.000  =>     0.000
   0.123  =>     0.123
  -0.123  =>    -0.123
 123.000  =>   123.000
-123.000  =>  -123.000