Reputation: 121
This is a simple program but I am unable to understand logic/work being done by while loop behind the scene.
Problem: Write a program that prints every number from 0 to 1 that has a single digit after the decimal place (that is, 0.1, 0.2, and so on).
So here is my code:
$num = 0;
while ( $num < 1 ) {
print "$num \n";
$num = $num + 0.1;
}
If I write it in this way, it is going to print
$num = 0;
while ( $num < 1 ) {
$num = $num + 0.1;
print "$num \n";
}
Output:
0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 1.1
Ideally speaking, 1 and 1.1 should not get printed in both code samples respectively. After printing 0.9 when 0.1 is added to it, it becomes 1.0 i.e. while (1.0 < 1)
. Hence condition in while loop is false, so it should not print 1 and 1.1. But that is what is happening.
Can someone please explain why while loop is working in this unexpected way i.e. printing 1 and 1.1 even when condition is false.
Upvotes: 2
Views: 265
Reputation: 386386
1/10 is a periodic number in binary just like 1/3 is periodic in decimal.
____
1/10 = 0.00011 base 2
As such, it can't be represented exactly by a floating-point number.
$ perl -e'printf "%$.20e\n", 0.1;'
1.00000000000000005551e-01
This imprecision is the cause of your problem.
$ perl -e'my $i = 0; while ($i < 1) { printf "%1\$.3f %1\$.20e\n", $i; $i += 0.1; }'
0.000 0.00000000000000000000e+00
0.100 1.00000000000000005551e-01
0.200 2.00000000000000011102e-01
0.300 3.00000000000000044409e-01
0.400 4.00000000000000022204e-01
0.500 5.00000000000000000000e-01
0.600 5.99999999999999977796e-01
0.700 6.99999999999999955591e-01
0.800 7.99999999999999933387e-01
0.900 8.99999999999999911182e-01
1.000 9.99999999999999888978e-01
Generally speaking, one can solve this by checking if the number is equal to another within some tolerance. But in this case, there's a simpler solution.
$ perl -e'for my $j (0..9) { my $i = $j/10; printf "%1\$.3f %1\$.20e\n", $i; }'
0.000 0.00000000000000000000e+00
0.100 1.00000000000000005551e-01
0.200 2.00000000000000011102e-01
0.300 2.99999999999999988898e-01
0.400 4.00000000000000022204e-01
0.500 5.00000000000000000000e-01
0.600 5.99999999999999977796e-01
0.700 6.99999999999999955591e-01
0.800 8.00000000000000044409e-01
0.900 9.00000000000000022204e-01
The above solution not only performs the correct number of iterations, it doesn't accumulate error, so $i
is always as correct as it can be.
Upvotes: 4
Reputation: 104065
Consider this:
use strict;
use warnings;
use v5.10;
my $num = 0;
while ($num < 1) {
say $num, " (", $num-1, ")";
$num += 0.1;
}
This outputs:
0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)
1 (-1.11022302462516e-16)
As you can see, due to floating point precision issues the number gained by repeated adding of 0.1 isn’t exactly one, but something slightly less than one, which is why the check succeeds one more time than expected.
Now add this to the imports at the top of the script:
use bignum;
And watch the output change:
0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)
This is because the computation is now done in a high-precision mode and the floating-point error no longer creeps in.
Usually, people get bitten a few times with floating point arithmetic and then learn to do the important part with integers or epsilons. Google, there’s plenty of resources on how to deal with floating point precision errors.
Upvotes: 2
Reputation: 26753
In the second version, you print before the loop checks, i.e. it will even print the case which is already beyond the loop condition.
The fact that you get 1.0 as well as 1.1 is explained by 1.0 being slightly higher than the result of adding a lot of 0.1, because of floating point precision.
So what the loop actually sees is
So in order to solve, use the first version but start at 0.1 and check against 0.95.
$num = 0.1;
while ( $num < 0.95 ) {
print "$num \n";
$num = $num + 0.1;
}
Upvotes: 4