Rounding floating point numbers
What does "rounding a floating point number" mean?
That's easy, obviously... Where's my math book from school...
No, we already know nothing related to floating point numbers is easy:
For a start, there are multiple rounding modes:
Rounding to zero?
Rounding to nearest - ties to even?
Rounding to nearest - ties away from zero?
How to handle the corner cases? How to find out which are the corner cases?
OK, looks like we better use an implementation of the IEEE 754 standard, and let our system take care of that.
To round a floating point number in the shell, based on standard floating point arithmetic, we need three steps:
- Convert the input text from a command line argument to a standard floating point number.
- Round the floating point number using the normal IEEE 754 implementation.
- Format the number as a string for output.
Turns out that the shell command
printf can do all of this. It can be used to print numbers according to a format specification as described in
man 3 printf. The numbers are rounded implicitly in the standard way if it is required for the output format:
p digits precision with input as command line arguments:
printf "%.*f\n" "$p" "$x"
Or in a shell pipeline, with input of
x on standard input, and
p as argument:
echo "$x" | xargs printf "%.*f\n" "$p"
$ printf '%.*f\n' 0 6.66
$ printf '%.*f\n' 1 6.66
$ printf '%.*f\n' 2 6.66
$ printf '%.*f\n' 3 6.66
$ printf '%.*f\n' 3 6.666
$ printf '%.*f\n' 3 6.6666
Beware the locale! It specifies the separator between the integral and fraction part - the
., as you may expect.
But see yourself what happens in a German locale, for example:
$ LC_ALL=de_DE.UTF-8 printf '%.*f\n' 3 6.6666
Yes, that's right
6,667 - six comma six six seven. That would mess up your script for sure.
(But only for the two customers in Germany. Except for the developer's machines currently debugging for these customers.)
To make it more robust, use:
LC_ALL=C /usr/bin/printf "%.*f\n" "$p" "$x"
echo "$x" | LC_ALL=C xargs /usr/bin/printf "%.*f\n" "$p"
This also uses
/usr/bin/printf instead of the shell builtin of
zsh to work around minor inconsistencies in implementation of the
printf variants, and prevent a very dirty effect when, in a German locale,
LC_ALL is set, but not exported. Then, the builtin uses
%g for rounding to a specified number of significant digits.