Thursday, January 6, 2011

Perl "for-loop" floating number issue and its solution

Today, a logic error in piece of my newly created Perl code caught my attention. Since similar codes worked well before,  I isolated them for a closer examination. It turned out to be a floating variable comparison issue that may give you unexpected results when used in a for-loop. Below is the sample codes for the debug and illustration purpose.

    print "\n1st for-loop: start -> -0.05; stop -> 0.15; step -> 0.05 ...\n";
    my $loop_cn=0;
    my $tmp_start=-0.05;
    my $tmp_stop=0.15;
    my $tmp_step=0.05;
    for(my $tmp=$tmp_start; $tmp<=$tmp_stop; $tmp+=$tmp_step)
    {
      $loop_cn++;
      print "loop $loop_cn meets condition $tmp <= $tmp_stop with a step of $tmp_step\n";
    }

    print "\n2nd for-loop: start -> -0.5; stop -> 1.5; step -> 0.5 ...\n";
    my $loop_cn=0;
    my $tmp_start=-0.5;
    my $tmp_stop=1.5;
    my $tmp_step=0.5;
    for(my $tmp=$tmp_start; $tmp<=$tmp_stop; $tmp+=$tmp_step)
    {
      $loop_cn++;
      print "loop $loop_cn meets condition $tmp <= $tmp_stop with a step of $tmp_step\n";
    }

    print "\n3rd for-loop: start -> -0.06; stop -> 0.1; step -> 0.02 ...\n";
    my $loop_cn=0;
    my $tmp_start=-0.06;
    my $tmp_stop=0.1;
    my $tmp_step=0.02;
    for(my $tmp=$tmp_start; $tmp<=$tmp_stop; $tmp+=$tmp_step)
    {
      $loop_cn++;
      print "loop $loop_cn meets condition $tmp <= $tmp_stop with a step of $tmp_step\n";
     # print sprintf("%.${10}g\n", $tmp);
     # print "\n";
    }

The execution results are shown as below:

1st for-loop: start -> -0.05; stop -> 0.15; step -> 0.05 ...
loop 1 meets condition -0.05 <= 0.15 with a step of 0.05
loop 2 meets condition 0 <= 0.15 with a step of 0.05
loop 3 meets condition 0.05 <= 0.15 with a step of 0.05
loop 4 meets condition 0.1 <= 0.15 with a step of 0.05

2nd for-loop: start -> -0.5; stop -> 1.5; step -> 0.5 ...
loop 1 meets condition -0.5 <= 1.5 with a step of 0.5
loop 2 meets condition 0 <= 1.5 with a step of 0.5
loop 3 meets condition 0.5 <= 1.5 with a step of 0.5
loop 4 meets condition 1 <= 1.5 with a step of 0.5
loop 5 meets condition 1.5 <= 1.5 with a step of 0.5

3rd for-loop: start -> -0.06; stop -> 0.1; step -> 0.02 ...
loop 1 meets condition -0.06 <= 0.1 with a step of 0.02
loop 2 meets condition -0.04 <= 0.1 with a step of 0.02
loop 3 meets condition -0.02 <= 0.1 with a step of 0.02
loop 4 meets condition 6.93889390390723e-018 <= 0.1 with a step of 0.02
loop 5 meets condition 0.02 <= 0.1 with a step of 0.02
loop 6 meets condition 0.04 <= 0.1 with a step of 0.02
loop 7 meets condition 0.06 <= 0.1 with a step of 0.02
loop 8 meets condition 0.08 <= 0.1 with a step of 0.02

It is very clear that the 1st and the 3rd loop is not working as expected while the 2nd loop works fine. Judged by the output of the 3rd for-loop “loop 4....” where the expected “0” is represented by a small number at the order of 1E-18, we know that the problem is caused by the inexact representation of the floating numbers due to limited machine precision. Thus, the attempt of using string comparison “le” in place of “<=” will not work either in this case.

Inspired by the solutions from:  http://docstore.mik.ua/orelly/perl4/cook/ch02_04.htm , this issue can be addressed following:
1. Create a sub-routine for numerical comparison named as less_eq
sub less_eq
{
    my ($A, $B, $dp) = @_;
    return sprintf("%.${dp}g", $A) <= sprintf("%.${dp}g", $B);
}
2. Replace the line $tmp<=$tmp_stop in the original code with less_eq($tmp,$tmp_stop,2)

The revised codes will give you the expected results for all cases as shown below:
 
1st for-loop: start -> -0.05; stop -> 0.15; step -> 0.05 ...
loop 1 meets condition -0.05 <= 0.15 with a step of 0.05
loop 2 meets condition 0 <= 0.15 with a step of 0.05
loop 3 meets condition 0.05 <= 0.15 with a step of 0.05
loop 4 meets condition 0.1 <= 0.15 with a step of 0.05
loop 5 meets condition 0.15 <= 0.15 with a step of 0.05
2nd for-loop: start -> -0.5; stop -> 1.5; step -> 0.5 ...
loop 1 meets condition -0.5 <= 1.5 with a step of 0.5
loop 2 meets condition 0 <= 1.5 with a step of 0.5
loop 3 meets condition 0.5 <= 1.5 with a step of 0.5
loop 4 meets condition 1 <= 1.5 with a step of 0.5
loop 5 meets condition 1.5 <= 1.5 with a step of 0.5
3rd for-loop: start -> -0.06; stop -> 0.1; step -> 0.02 ...
loop 1 meets condition -0.06 <= 0.1 with a step of 0.02
loop 2 meets condition -0.04 <= 0.1 with a step of 0.02
loop 3 meets condition -0.02 <= 0.1 with a step of 0.02
loop 4 meets condition 6.93889390390723e-018 <= 0.1 with a step of 0.02
loop 5 meets condition 0.02 <= 0.1 with a step of 0.02
loop 6 meets condition 0.04 <= 0.1 with a step of 0.02
loop 7 meets condition 0.06 <= 0.1 with a step of 0.02
loop 8 meets condition 0.08 <= 0.1 with a step of 0.02
loop 9 meets condition 0.1 <= 0.1 with a step of 0.02


No comments:

Post a Comment