Bash – Format Floating Point Number with 2 Significant Digits

awkbashbcdcfloating point

I want to print the floating point number with exactly two significant digits in bash (maybe using a common tool like awk, bc, dc, perl etc.).

Examples:

  • 76543 should be printed as 76000
  • 0.0076543 should be printed as 0.0076

In both cases the significant digits are 7 and 6. I have read some answers for similar problems like:

How to round floating point numbers in shell?

Bash limiting precision of floating point variables

but the answers focus on limiting the number of decimal places (eg. bc command with scale=2 or printf command with %.2f) instead of significant digits.

Is there an easy way to format the number with exactly 2 significant digits or do I have to write my own function?

Best Answer

This answer to the first linked question has the almost-throwaway line at the end:

See also %g for rounding to a specified number of significant digits.

So you can simply write

printf "%.2g" "$n"

(but see the section below on decimal separator and locale, and note that non-Bash printf need not support %f and %g).

Examples:

$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077

Of course, you now have mantissa-exponent representation rather than pure decimal, so you'll want to convert back:

$ printf "%0.f\n" 7.7e+06
7700000

$ printf "%0.7f\n" 7.7e-06
0.0000077

Putting all this together, and wrapping it in a function:

# Function round(precision, number)
round() {
    n=$(printf "%.${1}g" "$2")
    if [ "$n" != "${n#*e}" ]
    then
        f="${n##*e-}"
        test "$n" = "$f" && f= || f=$(( ${f#0}+$1-1 ))
        printf "%0.${f}f" "$n"
    else
        printf "%s" "$n"
    fi
}

(Note - this function is written in portable (POSIX) shell, but assumes that printf handles the floating-point conversions. Bash has a built-in printf that does, so you're okay here, and the GNU implementation also works, so most GNU/Linux systems can safely use Dash).

Test cases

radix=$(printf %.1f 0)
for i in $(seq 12 | sed -e 's/.*/dc -e "12k 1.234 10 & 6 -^*p"/e' -e "y/_._/$radix/")
do
    echo $i "->" $(round 2 $i)
done

Test results

.000012340000 -> 0.000012
.000123400000 -> 0.00012
.001234000000 -> 0.0012
.012340000000 -> 0.012
.123400000000 -> 0.12
1.234 -> 1.2
12.340 -> 12
123.400 -> 120
1234.000 -> 1200
12340.000 -> 12000
123400.000 -> 120000
1234000.000 -> 1200000

A note on decimal separator and locale

All the working above assumes that the radix character (also known as the decimal separator) is ., as in most English locales. Other locales use , instead, and some shells have a built-in printf that respects locale. In these shells, you may need to set LC_NUMERIC=C to force the use of . as radix character, or write /usr/bin/printf to prevent use of the built-in version. This latter is complicated by the fact that (at least some versions) seem to always parse arguments using ., but print using the current locale settings.