Printing floating-point numbers in C using “engineering notation” and SI prefixes

The simple C routine presented here can be used to print a floating-point number in an engineering notation, i.e. so that the exponent of ten is evenly divisible by three. The routine also has the option of replacing the En part with an SI prefix. Thus, a value like 42001.5 would be printed as 42.0015E3 or 42.0015 k, depending on a parameter and assuming that at least six significant digits are requested for. (The routine has a parameter for specifying the number of significant digits.) The form 42.0015 k is intended for use with SI units, i.e. it is to be (immediately) followed by a unit symbol like m (for meter).

This page is not actively maintained. There is a GitHub activity on the topic: EngineeringNotationFormatter.

If we use a simple printf function invocation to print a floating-point number, say printf("%lf",42001.5), the output is something like 4.200150e+04. This is not convenient. It would be better to have the mantissa and the exponent scaled so that the mantissa is between 0.1 and 1000.0 and the exponent is a multiple of three, for example 42.00150e+03. This (or the corresponding notation to be used in text documents when superscripts style can be used: 42.00150×103) is the so-called engineering notation for floating-point numbers in textual format. Although there are contexts where one should deviate from that principle, it is in general recommendable, since it makes the presentation more readable; see e.g. section 7.9 Choosing SI prefixes in the Guide for the Use of the International System of Units (SI). In Fortran, from Fortran 90 onwards, one could use the EN formatting code (edit descriptor).

But in C, there is no direct way to request for such a format when using printf, so I wrote a simple function for it. Since the notation as defined above allows some values to be represented in different formats (e.g. 0.1234e6 and 123.4e3 would both be acceptable), I made the choice that the mantissa is in the interval [1.0,1000.0), i.e. greater than or equal to 1.0 and less than 1000.0. I also added the optional feature of replacing powers of ten by SI prefixes when possible.

The program

The program, which is also available as a separate file, is the following:

#define MICRO "µ"

#define PREFIX_START (-24)
/* Smallest power of then for which there is a prefix defined.
   If the set of prefixes will be extended, change this constant
   and update the table "prefix". */

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

char *eng(double value, int digits, int numeric)
{
  static char *prefix[] = {
  "y", "z", "a", "f", "p", "n", MICRO, "m", "",
  "k", "M", "G", "T", "P", "E", "Z", "Y"
  }; 
#define PREFIX_END (PREFIX_START+\
(int)((sizeof(prefix)/sizeof(char *)-1)*3))

      int expof10;
      static unsigned char result[100];
      unsigned char *res = result;

      if (value < 0.)
        {
            *res++ = '-';
            value = -value;
        }
      if (value == 0.)
        {
	    return "0.0";
        }

      expof10 = (int) log10(value);
      if(expof10 > 0)
        expof10 = (expof10/3)*3;
      else
        expof10 = (-expof10+3)/3*(-3); 
 
      value *= pow(10,-expof10);

      if (value >= 1000.)
         { value /= 1000.0; expof10 += 3; }
      else if(value >= 100.0)
         digits -= 2;
      else if(value >= 10.0)
         digits -= 1;

      if(numeric || (expof10 < PREFIX_START) ||    
                    (expof10 > PREFIX_END))
        sprintf(res, "%.*fe%d", digits-1, value, expof10); 
      else
        sprintf(res, "%.*f %s", digits-1, value, 
          prefix[(expof10-PREFIX_START)/3]);
      return result;
}

Comments on coding: The string %.*f in the format means fixed-point formatting so that the number of digits to the right of the decimal point is taken from an argument in the printf argument list (here digits-1). The adjustments to the value of digits take into account the number of digits to the right of the decimal point (in this case, 1 to 3).

Installation notes

The macro MICRO is defined as referring to MICRO SIGN character. If this does not work reliably on your system, use the conventional surrogate “u” for “µ”, by changing the definition to #define MICRO "u"

Note that the routine uses some functions (log10, pow) from the math library. On several systems, you need to take some special action to specify that the program uses math; in typical Unix implementations, you would append the option -lm to the command line used to compile and load the program (e.g. cc engtest.c eng.c -lm), but check the applicable system-specific documentation.

Usage

In a file where you call the eng function, the following prototype declaration is needed:

extern char *eng(double,int,int);

The meanings of the arguments and the function result are as follows: In eng(x,d,num),

For example, if variable eff contains a value which represents a quantity in watts, you could use printf("%sW",eng(eff,4,0)) to get it displayed with 4 significant digits, in a notation that contains the unit, e.g. 112.4 mW or 42.00 W or 1.017 kW or 142.6 MW. If you prefer presentations like 112.4e-3 W, you would use printf("%s W",eng(eff,4,1)) instead. Note: In the latter case, the function always includes the exponent part, even if it is e0.

Note that for area and volume quantities, you cannot just replace a power of ten by a prefix, since e.g. 42 mm² does not mean 42e-3 m² (but 42 (mm)², i.e. 42e-6 m²). So for such quantities, set the num to 1 calling eng.

The return value is a pointer to a data area which will be overwritten by the next call to the same function. Therefore, do not call eng twice in one statement.

Testing

You can test the function using the following simple test program:

#include <stdio.h>
extern char *eng(double,int,int);
int main(void)
{
      double w;
      for (w = 1e-27; w < 1e30; w *= 42) {
            printf("%g \t= %s", w, eng(w, 4, 1));
            printf(" \t= %s\n",    eng(w, 4, 0)); }
      return 0;
}

Sample output is available for comparison; it begins as follows:

1e-27      = 1.000e-27     = 1.000e-27
4.2e-26         = 42.00e-27     = 42.00e-27
1.764e-24       = 1.764e-24     = 1.764 y
7.4088e-23      = 74.09e-24     = 74.09 y
3.1117e-21      = 3.112e-21     = 3.112 z

Rounding enhancement

David Hoerl has pointed out that if the number is just an epsilon less than 1.0, we can end up with a display 1000.0e-3. The problem is that my code checks, among other things, whether the number is less than 1, and if it is, selects a presentation with a negative power of ten. This would generate e.g. for 0.9999 the presentation 999.9e-3 as intended. In this case, however, the number is less than 1 when testing the condition but gets rounded by the printf function. The presentation is mathematically correct but not the intended one.

Mr. Hoerl (e-mail: dhoerl at mac dot com; replace “at” by “@” and “dot” by “.” and omit spaces) suggests adding additional processing to essentially round the value to the intended precision before the type of presentation is selected. In addition, instead of the static array, he suggests the use of the asprintf function, which mallocs memory that must then later be freed. His code is the following:

static char *eng(double value, int digits, int numeric)
{
     double display, fract;
     int expof10;
     char *result, *sign;

     assert(isnormal(value)); // could also return NULL

     if(value < 0.0) {
         sign = "-";
         value = -value;
     } else {
         sign = "";
     }

     // correctly round to desired precision
     expof10 = lrint( floor( log10(value) ) );
     value *= pow(10.0, digits - 1 - expof10);

     fract = modf(value, &display);
     if(fract >= 0.5) display += 1.0;

     value = display * pow(10.0, expof10 - digits + 1);


     if(expof10 > 0)
         expof10 = (expof10/3)*3;
     else
         expof10 = ((-expof10+3)/3)*(-3);

     value *= pow(10.0, -expof10);
     if (value >= 1000.0) {
         value /= 1000.0;
         expof10 += 3;
     }
     else if(value >= 100.0)
         digits -= 2;
     else if(value >= 10.0)
         digits -= 1;

     if(numeric || (expof10 < PREFIX_START) || (expof10 > PREFIX_END))
         asprintf(&result, "%s%.*fe%d", sign, digits-1, value, expof10);
     else
         asprintf(&result, "%s%.*f %s", sign, digits-1, value,
prefix[(expof10-PREFIX_START)/3]);

     return result;
}

Date of creation: 2000-07-05. Last update: 2013-04-21.

Jukka Korpela