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 `E`n 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)`,

• x is the floating-point number to be presented
• d is an integer specifying the desired number of significant digits
• num is a flag (0 = false, 1 = true) which can be set if you wish to have all values presented purely numerically; if the flag is not set, SI prefixes are used instead of powers of ten when possible
• the function return value, i.e. the value of the expression `eng(x,d,num)`, is a pointer to string representing x with d significant digits using the engineering notation discussed here.

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