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)
,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, 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).
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.
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)
,
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.
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
David Hoerl has pointed out
that if the number is
just an epsilon less than 1.0, we can end up with a display
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