7

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. – Gilles Nov 1 '15 at 23:30
  • @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... – Dreaming in Code Nov 2 '15 at 1:45
13

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

4

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.

1

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.

0

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.