# Weird float rounding behavior with printf

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).

• Where do you see a bug? 196 is a perfectly sensible rounding of 196.5 to 0 places after the decimal point, and ditto for 198 for 197.5. Are you wondering why it sometimes rounds up and sometimes down? That normal since this is rounding, not truncating. Nov 1, 2015 at 23:30
• Nov 1, 2015 at 23:32
• @Gilles Well I mean there is a bug in my own script related to floating point arithmetic because it did not behave as what the real scenario requires, and I know this is of course not a bug in IEEE 754... Nov 2, 2015 at 1:45

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`

It is not a bug, it is intentional.
It is doing a type of round to nearest (more on that later).
With exactly `.5` we can round either way. At school you where probably told to round up, but why? Because you then do not have to examine any more digits e.g. 3.51 round up to 4; 3.5 could go ether way, but if we only look at first digit and round .5 up then we always get it right.

However, if we look at the set of 2 digit decimals: 0.00 0.01, 0.02, 0.03 … 0.98, 0.99, we will see there are 100 values, 1 is an integer, 49 have to be rounded up, 49 have to be rounded down, 1 ( 0.50 ) could go ether way. If we always round up, then we get on average numbers that are 0.01 too big.

If we extend the range to 0 → 9.99, we have 9 value extra that round up. Thus making our average a bit bigger than expected. So one attempt at fixing this is: .5 rounds towards even. Half the time it rounds up, half the time it rounds down.

This changes the bias from upwards, to towards even. In most cases this is better.

Temporarily changing rounding modes is not that unusual and it is possible with `bin/printf` although not per se you need to change the sources.

You need the sources of the coreutils, I used the latest version available today which was http://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz.

Unpack into a directory of your choice with

`tar xJfv coreutils-8.24.tar.xz`

Change into the source-directory

`cd coreutils-8.24`

Load the file `src/printf.c` into the editor of your choice and exchange the whole `main` function with the following function including both of the preprocessor directives to include the header files `math.h` and `fenv.h`. The main function is at the end and starts at `int main...` and ends at the very end of the file with the closing bracket `}`

``````#include <math.h>
#include <fenv.h>
int
main (int argc, char **argv)
{
char *format;
char *rounding_env;
int args_used;
int rounding_mode;

initialize_main (&argc, &argv);
set_program_name (argv[0]);
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);

atexit (close_stdout);

exit_status = EXIT_SUCCESS;

posixly_correct = (getenv ("POSIXLY_CORRECT") != NULL);
// accept rounding modes from an environment variable
if ((rounding_env = getenv ("BIN_PRINTF_ROUNDING_MODE")) != NULL)
{
rounding_mode = atoi(rounding_env);
switch (rounding_mode)
{
case 0:
if (fesetround(FE_TOWARDZERO) != 0)
{
error (0, 0, _("setting rounding mode to roundTowardZero failed"));
return EXIT_FAILURE;
}
break;
case 1:
if (fesetround(FE_TONEAREST) != 0)
{
error (0, 0, _("setting rounding mode to roundTiesToEven failed"));
return EXIT_FAILURE;
}
break;
case 2:
if (fesetround(FE_UPWARD) != 0)
{
error (0, 0, _("setting rounding mode to roundTowardPositive failed"));
return EXIT_FAILURE;
}
break;
case 3:
if (fesetround(FE_DOWNWARD) != 0)
{
error (0, 0, _("setting rounding mode to roundTowardNegative failed"));
return EXIT_FAILURE;
}
break;
default:
error (0, 0, _("setting rounding mode failed for unknown reason"));
return EXIT_FAILURE;
}
}
/* We directly parse options, rather than use parse_long_options, in
order to avoid accepting abbreviations.  */
if (argc == 2)
{
if (STREQ (argv[1], "--help"))
usage (EXIT_SUCCESS);

if (STREQ (argv[1], "--version"))
{
version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS,
(char *) NULL);
return EXIT_SUCCESS;
}
}

/* The above handles --help and --version.
Since there is no other invocation of getopt, handle '--' here.  */
if (1 < argc && STREQ (argv[1], "--"))
{
--argc;
++argv;
}

if (argc <= 1)
{
error (0, 0, _("missing operand"));
usage (EXIT_FAILURE);
}

format = argv[1];
argc -= 2;
argv += 2;

do
{
args_used = print_formatted (format, argc, argv);
argc -= args_used;
argv += args_used;
}
while (args_used > 0 && argc > 0);

if (argc > 0)
error (0, 0,
_("warning: ignoring excess arguments, starting with %s"),
quote (argv[0]));

return exit_status;
}
``````

Run `./configure` as follows

`LIBS=-lm ./configure --program-suffix=-own`

It puts the suffix `-own` at every subprogram (there are a lot) just in case you want to install them all and are unsure if they fit with the rest of the system. The coreutils are not named coreutils without a reason!

But the most important is the `LIBS=-lm` in front of the line. We need the mathematical library and this command tells `./configure` to add it to the list of needed libraries.

Run make

`make`

If you have a multicore/multiprocessor system try

`make -j4`

where the number (here "4") should represent the number of cores you are willing to spare for that job.

If all went well you have the new `printf` int `src/printf`. Try it out:

`BIN_PRINTF_ROUNDING_MODE=1 ./src/printf '%.0f\n' 196.5`

`BIN_PRINTF_ROUNDING_MODE=2 ./src/printf '%.0f\n' 196.5`

Both command should differ in output. The numbers after `IN_PRINTF_ROUNDING_MODE` mean:

• 0 Rounding toward 0
• 1 Rounding toward nearest number (default)
• 2 Rounding toward positive infinity
• 3 Rounding toward negative infinity

You may install the whole (not recommended) or just copy the file (renaming it before is highly recommended!) `src/printf` into a directory in your `PATH` and use as described above.

You can do the following short one liner if what you actually want is to round down for x.1 to x.4 and round up for x.5 to x.9.

`if [[ \${a#*.} -ge "5" ]]; then a=\$((\${a%.*}+1)); else a=\${a%.*}; fi`

Or change "5" to whatever you want, e.g. "6".

P.S. regarding the issue with "." and/or "," being used as the decimal seperator(s), here is an easy universal solution.

```if [[ \${a##*[.,]} -ge "5" ]]; then a=\$((\${a%[.,]*}+1)); else a=\${a%[.,]*}; fi```