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

This is my first post here, so please excuse my bad english.

Working on a larger project we needed some code to round numbers (that are prices in a shop) to a precision of 2 digits (%.2f), i.e. from 8.7385 to 8.74. In 'Programming perl', or in the monastery you find this code:
sub round($$) { sprintf "%.$_[1]f",$_[0]; } print round(8.7385, 2); # prints 8.74
which works really nice... until we found out following strange behaviour:
for my $i (0 .. 10) { print round($i+0.555,2)," "; } # which prints: # 0.56 1.56 2.56 3.56 4.55 5.55 6.55 7.55 8.55 9.55 10.56
...hm, the decimal part should allways be the same!
from 10 upwards it seems to stay '.56', but now look at this:
for (my $i = 1; $i < \xFFFF; $i <<= 1) { print round($i+0.555,2), " "; } # which prints: #1.56 2.56 4.55 8.55 16.56 32.56 64.56 128.56 256.56 512.55 #1024.56 2048.55 4096.56 8192.56 16384.56 32768.56 65536.55 #131072.55 262144.55 524288.56 1048576.55 2097152.56 4194304.55 #8388608.55 16777216.56 # #(You can try with different numbers and different precision as long # as the last digit is '5' and the precision = number of digits - 1)
So here goes my first question:

- Is there a 'mystical rule' behind this behaviour? (Is ist a bug or is it a feature?)
(Btw. i've tested on Linux and different Windows Systems, Activestate 5.005/5.6.)

And the 2nd question:

I wrote this code to achieve same behaviour (and allways print %.2f in the shop):
sub round2($$) { my $var=$_[0]; my $decimal=($var-int(var)); if ($decimal && ((length($decimal)-2)>$_[1]) && ($var=~/5$/)) { $var=~s/5$/6/; } sprintf "%.$_[1]f",$var; } # some output: for (my $i = 1; $i < \xFFFF; $i <<= 1) { print round($i+0.555,2), " ", round2($i+0.555,2), "\n"; }
...but it's somewhat obfuscated (and it does not take care of numbers like 1.5E-4).

So any ideas or comments would be appreciated.

Replies are listed 'Best First'.
Re: strange rounding behaviour
by stephen (Priest) on May 18, 2001 at 20:00 UTC
    You've found the hidden indeterminacy of floating-point numbers.

    Because of the way that most machines handle floating-points, they're frequently not what they say they are. From PerlFaq 4:

    Internally, your computer represents floating-point numbers in binary. Floating-point numbers read in from a file or appearing as literals in your program are converted from their decimal floating-point representation (eg, 19.95) to the internal binary representation.

    However, 19.95 can't be precisely represented as a binary floating-point number, just like 1/3 can't be exactly represented as a decimal floating-point number. The computer's binary representation of 19.95, therefore, isn't exactly 19.95.

    If you really, really need exact handling of floating-point numbers, you can (at a good deal of computational expense) use Math::BigFloat. Numbers which use Math::BigFloat are internally handled as strings, so you won't get the strange rounding errors you're experiencing. Plus, it provides a bround method that does the exact rounding you want.

    stephen

      Or, if you know exactly how many decimal places you need, you can scale it up by that magnitude, and use ints instead.

      I.e., if you need 2 decimal places, use ints * 100, and print like this:

      print ($a/100),".","($a%100),"\n";

        I had written a well thought out reply to this, but then by browser crashed, so I'll just provide some points on why this script fails:

      • First of all, it's syntactically invalid (this, I assume, is just a type): you provide a " before ($a%100), I assume that isn't meant to be there. Also, you need to place parentheses around your print call so all of the items you want to send go to print.
      • Second, It doesn't account for the fact that the computer cannot store the floating point numbers accurately.
      • Third, it produces output like this: (when the typos are corrected)
        1.23.23 4.345.34 5.523.52
        You see, you print ($a/100), but if that is a decimal, then it will print (decimal and all!), and then the added decimal will print, not what you want.
      • Last, it doesn't round. The middle number should read 4.35 (after being rounded), but your code (ignoring the extra decimal and just reading from the last), makes it 4.34.

        The 15 year old, freshman programmer,
        Stephen Rawls
Re: strange rounding behaviour
by lhoward (Vicar) on May 18, 2001 at 19:38 UTC
    I always round like this:
    #round $num to 2 decimal places $rounded = int(0.5+$num*100)/100;
    That code doesn't seem to exhibit the strange behavior you see when sprintf for rounding.

    In case you care about performance, the INT version I describe is more than 2.5 times as fast as the sprintf version:

    Benchmark: running RoundINT, RoundSPRINTF, each for at least 3 CPU seconds...
      RoundINT:  3 wallclock secs ( 3.15 usr +  0.00 sys =  3.15 CPU) @ 7753.02/s (n=24422)
    RoundSPRINTF:  3 wallclock secs ( 3.21 usr +  0.00 sys =  3.21 CPU) @ 2906.85/s (n=9331)
    
      print int(0.5 + 0.695*100)/100;
      prints 0.7 for me. Thereby showing that your round does not get the formatting correct which sprintf does, and still suffers from inconsistent rounding behaviour.

      The reasons for this behaviour are basic to the fact of how floating point numbers work. They may not be readily predictable (exact details depend on your chip, OS, compiler, etc) but the underlying issue is that the internal representation of a floating point number is in base 2, while the visible one is in base 10. Therefore when you go to represent a floating point number, a nice round number base 10 turns into a repeating representation that is always stored with round-off error. What way you round in the boundary case depends on the interaction of that error and your operations.

        Nice catch tilly. I missed that case.

        My opinion boils down to this: use string formatting routines for formatting strings (not rounding numbers), and use numeric tools for numeric operations (like rounding). Using a string operator (like sprintf) to do a numeric function (rounding) is just asking for "unexpected complications".