grinder has asked for the wisdom of the Perl Monks concerning the following question:

Hmm, am I going nuts, or have I missed something obvious? I just replied to a question about rounding numbers, and came across something rather odd. It seems that sprintf doesn't always round correctly. I've been able to reproduce the behaviour on a number of architectures and versions of Perl. Consider (some awful code I just hacked up):

% cat sprin #! /usr/bin/perl -w use strict; for( 0..9 ) { my $num0 = "1.$_"; my $num1 = "1.0$_"; my $num2 = "1.00$_"; printf "$num0 => %0.0f $num1 => %0.1f $num2 => %0.2f\n", $num0, $num1, $num2; }

When this is run, it produces the following output. Look at the boundary condition when $_ == 5:

% perl sprin 1.0 => 1 1.00 => 1.0 1.000 => 1.00 1.1 => 1 1.01 => 1.0 1.001 => 1.00 1.2 => 1 1.02 => 1.0 1.002 => 1.00 1.3 => 1 1.03 => 1.0 1.003 => 1.00 1.4 => 1 1.04 => 1.0 1.004 => 1.00 1.5 => 2 1.05 => 1.1 1.005 => 1.00 1.6 => 2 1.06 => 1.1 1.006 => 1.01 1.7 => 2 1.07 => 1.1 1.007 => 1.01 1.8 => 2 1.08 => 1.1 1.008 => 1.01 1.9 => 2 1.09 => 1.1 1.009 => 1.01

I would tend to call this a bug. I wrote the following C program to see how it behaves (don't laugh, I haven't written any C from scratch in something like 8 years).

include <stdlib.h> #include <stdio.h> int main( int argc, char **argv ) { char *fmt = *++argv; double f = atof( *++argv ); char buf[128]; sprintf( buf, "%%s => %s\n", fmt ); printf( buf, *argv, f ); return 0; }

This lets me observe the following:

% ./s %0.0f 0.4 0.4 => 0 % ./s %0.0f 0.5 0.5 => 0 % ./s %0.0f 0.6 0.6 => 1 % ./s %0.0f 1.4 1.4 => 1 % ./s %0.0f 1.5 1.5 => 2 % ./s %0.0f 1.6 1.6 => 2

Therefore the problem seems to be down at the C level (after all, Perl's configure script does say that it is at the mercy of C's definitions for this and that). Is it therefore wrong to depend on sprintf for all your rounding needs? I am aware of floor and ceil, but that's rarely what I want. I want rounding. Strictly less than .5, round down, greater or equal to .5, round up. I'd always assumed that sprintf does that. If it doesn't, what does?


print@_{sort keys %_},$/if%_=split//,'= & *a?b:e\f/h^h!j+n,o@o;r$s-t%t#u'

Replies are listed 'Best First'.
•Re: counter-intuitive sprintf behaviour
by merlyn (Sage) on Jan 13, 2003 at 17:40 UTC
    Besides all the answers in this thread so far of "binary representation of decimal fractions is inexact", I saw a few puzzles that were cleanly answered by another often-forgotten fact:
    Rounding of a value that is exactly halfway between two whole numbers will always go to the nearest even number.
    This is mandated by mathematicians in an attempt to reduce round-off error.

    -- Randal L. Schwartz, Perl hacker
    Be sure to read my standard disclaimer if this is a reply.

      Rounding of a value that is exactly halfway between two whole numbers will always go to the nearest even number.
      Only when the radix/2 is odd (e.g. base 2 or base 10)
      When the radix is a multiple of 4, rounding to the nearest odd number is preferred.
Re: counter-intuitive sprintf behaviour
by dragonchild (Archbishop) on Jan 13, 2003 at 16:30 UTC
    <off-the-cuff answer>

    This sounds like it's not a problem with sprintf in either the Perl or C incarnations. It's probably more of a problem with how floats are represented in binary. 0.5, for example, may actually be stored as 0.499999999993, or something like that. That rounds to 0. 1.5 may actually be stored as 1.50000004, which rounds to 2.

    </off-the-cuff answer>

    ------
    We are the carpenters and bricklayers of the Information Age.

    Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

      No, 0.5 can be represented in binary exactly. 0.05 can't
Re: counter-intuitive sprintf behaviour
by pfaut (Priest) on Jan 13, 2003 at 16:38 UTC

    As stated by dragonchild, representation of floating point numbers is inexact. I added three lines to your program that show this.

    #! /usr/bin/perl -w use strict; for( 0..9 ) { my $num0 = "1.$_"; my $num1 = "1.0$_"; my $num2 = "1.00$_"; #next three lines were added $num0 -= 1; $num1 -= 1; $num2 -= 1; printf "$num0 => %0.0f $num1 => %0.1f $num2 => %0.2f\n", $num0, $num1, $num2; }

    This produces the following output.

    0 => 0 0 => 0.0 0 => 0.00 0.1 => 0 0.01 => 0.0 0.00099999999999989 => 0.00 0.2 => 0 0.02 => 0.0 0.002 => 0.00 0.3 => 0 0.03 => 0.0 0.00299999999999989 => 0.00 0.4 => 0 0.04 => 0.0 0.004 => 0.00 0.5 => 0 0.05 => 0.1 0.00499999999999989 => 0.00 0.6 => 1 0.0600000000000001 => 0.1 0.00600000000000001 => 0.01 0.7 => 1 0.0700000000000001 => 0.1 0.0069999999999999 => 0.01 0.8 => 1 0.0800000000000001 => 0.1 0.00800000000000001 => 0.01 0.9 => 1 0.0900000000000001 => 0.1 0.0089999999999999 => 0.01
    --- print map { my ($m)=1<<hex($_)&11?' ':''; $m.=substr('AHJPacehklnorstu',hex($_),1) } split //,'2fde0abe76c36c914586c';
Re: counter-intuitive sprintf behaviour
by Hofmator (Curate) on Jan 13, 2003 at 16:37 UTC
    Have a look at the output of the following code, then you'll understand. The problem is with the conversion of literals to floating point numbers, there 1.005 might in fact be 1.004999999999 which rounds to 1.00:
    for( 0..9 ) { my $num0 = "1.$_"; my $num1 = "1.0$_"; my $num2 = "1.00$_"; printf "$num0 => %0.0f $num1 => %0.1f $num2 => %0.2f\n", $num0, $num1, $num2; printf "$num0 => %0.20f $num1 => %0.20f $num2 => %0.20f\n", $num0, $num1, $num2; print "\n"; }

    -- Hofmator

Re: counter-intuitive sprintf behaviour
by impossiblerobot (Deacon) on Jan 13, 2003 at 20:29 UTC

    There are several good nodes on this issue floating around the monastery.

    Here's one that I found helpful.


    Impossible Robot