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

Just recently, a co-worker and I were discussing code optimization and "preferred" methods of performing common tasks in perl. The code in question works but might not be the best or the recommended way of doing it.

So, this question concerns rounding. Here is said code:
sub roundThatBadDog { my $seconds = shift; return int ($seconds + .5); }
Not used for any heavy duty rounding, round up if >= 5 otherwise round down.

So, is there another preferred or better way of simple rounding?

Replies are listed 'Best First'.
Re: Preferred Methods
by japhy (Canon) on Jan 14, 2002 at 09:22 UTC
    Well, that only rounds properly for positive numbers.
    sub round { my $n = shift; int($n + .5 * ($n < 0 ? -1 : 1)); }
    Then you might want to be able to round to a specific place:
    sub round { my ($n, $p) = @_; $p ||= 0; # default to integer rounding int($n * 10**$p + .5 * ($n < 0 ? -1 : 1)) / 10**$p; }
    Ta da. Now you can round 123.45678 to 123.457 by calling round(123.45678, 3), and you can round 123456789 to 123500000 by calling round(123456789, -5).

    _____________________________________________________
    Jeff[japhy]Pinyan: Perl, regex, and perl hacker.
    s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

      I like your round() function, though I'd have it take a "nearest x" parameter instead of a "number of digits" parameter. Just replace the ||=0 with ||=1 and 10**$p with $p. Then you can say round($inches, 1/8), or round($bagles, 12).

      Thanks,
      James Mastros,
      Just Another Perl Scribe

Re: Preferred Methods
by blakem (Monsignor) on Jan 14, 2002 at 09:19 UTC
Re: Preferred Methods
by rob_au (Abbot) on Jan 14, 2002 at 09:22 UTC
    There is nothing wrong with your approach at all - Another method might be to employ the sprintf operator for which there is an excellent tutorial on this site. eg.

    sub roundThatBadDog { my $seconds = shift; return sprintf("%d", $seconds); }

    Or alternatively, if you are wanting precision, replace the %d with %.xf where x is a integer value representing the order of decimal precision desired.

     

    Update

     

    In response to japhy's comments, I checked out the FAQ and ran a few timing tests to compare.

    From my reading, the comments in the FAQ hardly suggest the avoidance of sprintf function for rounding purposes but rather the use of a more intrinsic, known method of rounding (such as those submitted by japhy here) for sensitive or high-value operations. The example given is a financial environment where the FAQ rightly suggests that a programmer should implement their own rounding code rather than relying on system functions.

    With regard to a timing comparison, the following is the output - Make of it what you will :-)

    Benchmark: timing 100000 iterations of japhy, rob_au... japhy: 3 wallclock secs ( 4.13 usr + 0.00 sys = 4.13 CPU) @ 24 +213.08/s (n=100000) rob_au: 4 wallclock secs ( 4.23 usr + 0.00 sys = 4.23 CPU) @ 23 +640.66/s (n=100000)

     

    Update

     

    These results may not be truly indicative of function performance, and may I add, not intendedly so. japhy discusses performance differences in his more comprehensive comparison here. ++japhy!

     

      Your benchmark is misleading, since you are comparing my rounding function (made in-place) with sprintf "%d", which does not round -- it is the equivalent (albeit a slower one) of int(), which truncates, not rounds.

      Restructuring the benchmark, I find vastly different results:

      #!/usr/bin/perl use Benchmark 'cmpthese'; use strict; my @n = (1.24, 5.43, -98.54, -73.667, 0.67, 2.34, 76.89, -999.99, 34.5 +2); sub round { my ($n, $p) = @_; $p ||= 0; int($n * 10**$p + .5 * ($n < 0 ? -1 : 1)) / 10**$p; } cmpthese(-5, { rob_int => sub { for (@n) { my $x = sprintf "%d", $_ } }, rob_round => sub { for (@n) { my $x = sprintf "%.0f", $_ } }, japhy_int => sub { for (@n) { my $x = int $_ } }, japhy_round => sub { for (@n) { my $x = round($_,0) } }, japhy_inplace => sub { for (@n) { my $x = int($_ + .5 * ($_ < 0 ? -1 : 1)) } }, });
      The results are thus:
      Benchmark: running japhy_int, japhy_round, japhy_inplace, rob_int, rob_round for at least 5 CPU seconds... japhy_int: 20326.40/s (n=108543) japhy_round: 3272.93/s (n= 17412) japhy_inplace: 11470.71/s (n= 61483) rob_int: 11797.18/s (n= 62643) rob_round: 1964.03/s (n= 10429) Rate rob_round japhy_round japhy_inplace rob_int japh +y_int rob_round 1964/s -- -40% -83% -83% + -90% japhy_round 3273/s 67% -- -71% -72% + -84% japhy_inplace 11471/s 484% 250% -- -3% + -44% rob_int 11797/s 501% 260% 3% -- + -42% japhy_int 20326/s 935% 521% 77% 72% + --
      What this all means is that, when it comes to truncating, it is about 75% faster to use int() and it is to use sprintf("%d"). It also shows that my function is 67% faster than using sprintf("%.0f") (which, I remind you, returns IEEE values which may not be appropriate for your computations). It also shows that if you were to inline my function call, you would run 250% faster -- sigh, the overhead of function calls.

      This data also supports the previous finding, that my function, made in-place, runs about as fast as sprintf("%d").

      _____________________________________________________
      Jeff[japhy]Pinyan: Perl, regex, and perl hacker.
      s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

      Using sprintf() for rounding in a value-sensitive environment is warned against in the FAQ itself. Not to mention mathematical rounding functions are far faster.

      _____________________________________________________
      Jeff[japhy]Pinyan: Perl, regex, and perl hacker.
      s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

Re: Preferred Methods
by gav^ (Curate) on Jan 14, 2002 at 10:23 UTC
    I've always gone for:
    use POSIX; sub round { floor($_[0] + 0.5) }
    I've always wondered why Perl doesn't include a round function, it is something that tends to stump beginners. I also wondered if this was faster than japhy's code.
    Benchmark: timing 100000 iterations of round1, round2... round1: 65 wallclock secs (64.59 usr + 0.00 sys = 64.59 CPU) @ 15 +48.23/s (n=100000) round2: 47 wallclock secs (47.78 usr + 0.00 sys = 47.78 CPU) @ 20 +92.93/s (n=100000)
    And it seems to be slightly faster. I think it would also be marginally more understandable from a maintanace standpoint.

    Also remember that if you work with floating point numbers you might get things you don't expect, for example, adding 0.01 to 0 99 times gives you 0.990000000000001.

    The module Math::FixedPrecision seems promising, but unfortunatly it rounds 0.50 to 0 rather than 1.

    use Math::FixedPrecision; for (my $i = Math::FixedPrecision->new(0); $i <= 1; $i += 0.01) { print $i, "\t", Math::FixedPrecision->new($i + 0.01, 0), "\n"; }
    I know I've gone off on a slight tangent here.. but is there some nicer way to deal with this. I haven't used Math::Currency but that does look promising.

    gav^

      The module Math::FixedPrecision seems promising, but unfortunatly it rounds 0.50 to 0 rather than 1.

      as far as i'm aware, rounding 'rules' are just conventions. they're only called rules because they provide a set of instructions.

      the method i was taught was that in cases of .5 exactly, the number would round to the nearest even number. so, 0.5000000000001 rounds to 1, 0.5 rounds to 0.

      ~Particle