2

I have this code:

sub range {
        my ($start, $end, $step) = @_;

        if($step == 0) {
                die("Step size cannot be 0!")
        }
        if($start > $end) {
                ($start, $end) = ($end, $start);
        }
        my @range = ();
        for (my $i = $start; $i <= $end; $i += $step) {
                push @range, $i;
        }

        return @range;
}

When running

my @range = range(-3, -2.7, 0.01);

I get this list:

...
$VAR23 = '-2.78';
$VAR24 = '-2.77';
$VAR25 = '-2.76000000000001';
$VAR26 = '-2.75000000000001';
$VAR27 = '-2.74000000000001';
$VAR28 = '-2.73000000000001';
$VAR29 = '-2.72000000000001';
$VAR30 = '-2.71000000000001';
$VAR31 = '-2.70000000000001';

Why does this happen?

I have perl v5.24.1 on a 4.9.0-7-amd64 #1 SMP Debian 4.9.107-1 machine. Adding the bignum module does not change the fact that the variables calculated are wrong.

Also, this does not happen when doing sometheng like "-2.7 - 0.01".

Rudy Velthuis
  • 28,387
  • 5
  • 46
  • 94
  • please could you show us an example of your call to range. e.g., range(1,10,1); – hoffmeister Jul 08 '18 at 16:43
  • I think this is a display issue with floats. try running it with printf("%.2f\n" , $_) for @range; instead of Data::Dumper – hoffmeister Jul 08 '18 at 17:12
  • Interestingly `my @range = map { -3 + $_/100 } 0..30` does not give me round-off errors – Håkon Hægland Jul 08 '18 at 17:35
  • 1
    @Håkon Hægland, Sure it does. It's impossible to store a number that's periodic in binary into a floating point number. You can see the error with `printf("%.20g", $_);`. It does have the benefit of avoiding error *accumulation*. – ikegami Jul 08 '18 at 23:08
  • 2
    @JonathanGreszwinsky The problem is that 1/10 is periodic in binary (just like 1/3 is in decimal), so it can't be stored accurately in a floating point number. On top of that, you make the problem worse by repeatedly adding inaccurate numbers. – ikegami Jul 08 '18 at 23:09

1 Answers1

2

Why does this happen?

See What Every Programmer Should Know About Floating-Point Arithmetic.

Adding the bignum module does not change the fact that the variables calculated are wrong.

I'm guessing that you added the use bignum; inside of sub range, but because bignum is scoped, this won't automatically affect the variables passed into that sub. So either bignum needs to be in effect wherever the literals that you pass into range are defined and it'll work, or, you could just upgrade the variables in the sub itself, as in:

use Math::BigRat;
sub range {
    my $start = Math::BigRat->new(shift);
    my $end   = Math::BigRat->new(shift);
    my $step  = Math::BigRat->new(shift);
    ...
    return map {$_->numify} @range;
}

However, using the Math::BigRat, Math::BigFloat, etc. (including via the bignum and related pragmas) objects everywhere will slow down the code, so this may be overkill. So that the objects are used only within the sub above, I'm downgrading the objects back into regular scalars with numify, but that's optional depending on your performance requirements.

Depending on what precision you actually need, you could also round your numbers off via e.g. sprintf("%.2f",$i) (just as an example: for (my $i = $start; $i <= $end; $i = 0+sprintf("%.2f",$i+$step) )). Another possibility, as pointed out by @ikegami in the comment, would be to work with integers and do the division last, as in e.g. map { $_/100 } -300 .. -270.

haukex
  • 2,973
  • 9
  • 21
  • 1
    BigFloat would still suffer form the same problem. No matter how big you make the float, you can't store a number that's periodic in binary into a float. BigRat could work, but that's surely overkill. `map { $_ / 0.01 } -300 .. -270` would avoid error accumulation, and rounding or tolerances can handle the rest. – ikegami Jul 08 '18 at 23:04
  • @ikegami Thanks, edited. The extra `use bignum;` was a vestige from my test code that I forgot to remove, and I switched my example code over to `Math::BigRat` because my point was to show how to do it with as much accuracy as possible, but I highlighted that it may be overkill. – haukex Jul 09 '18 at 06:35