Bash Shell Script – Weird Float Rounding Behavior with Printf

bashfloating pointmathprintfshell-script

I've read some answers on this site and found the printf rounding desirable.

However when I used it in practice, a subtle bug led me to the following behavior:

$ echo 197.5 | xargs printf '%.0f'
198
$ echo 196.5 | xargs printf '%.0f'
196
$ echo 195.5 | xargs printf '%.0f'
196

Notice that rounding 196.5 becomes 196.

I know this can be some subtle floating point bug (but this is not a very large number, huh?), so can someone throw some light upon this?

An workaround for this is also greatly welcomed (because I'm trying to put this to work now).

Best Answer

It is as expected, it is "round to even", or "Banker's rounding".

A related site answer explain it.

The issue that such rule is trying to solve is that (for numbers with one decimal),

  • x.1 up to x.4 are rounded down.
  • x.6 up to x.9 are rounded up.

That's 4 down and 4 up.
To keep the rounding in balance, we need to round the x.5

  • up one time and down the next.

That is done by the rule: « Round to nearest 'even number' ».

In code:

LC_NUMERIC=C printf '%.0f ' "$value"
echo "$value" | awk 'printf( "%s", $1)'


Options:

In total, there are four possible ways to round a number:

  1. The already explained Banker's rule.
  2. Round towards +infinite. Round up (for positive numbers)
  3. Round towards -infinite. Round down (for positive numbers)
  4. Round towards zero. Remove the decimals (either positive or negative).

Up

If you do need "round up (toward +infinite)", then you can use awk:

value=195.5

echo "$value" | awk '{ printf("%d", $1 + 0.5) }'
echo "scale=0; ($value+0.5)/1" | bc

Down

If you do need "round down (Toward -infinite)", then you can use:

value=195.5

echo "$value" | awk '{ printf("%d", $1 - 0.5) }'
echo "scale=0; ($value-0.5)/1" | bc

Trim decimals.

To remove the decimals (anything after the dot).
We could also directly use the shell (works on most shells - is POSIX):

value="127.54"    ### Works also for negative numbers.

echo "${value%%.*}"
echo "$value"| awk '{printf ("%d",$0)}'
echo "scale=0; ($value)/1" | bc

Related Question