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

hello, i find something strange, but as i don't know how to handle it, i ask here .... hoping for some answer

i have this code (simplified) i tested it under Perl v5.10.1 and v5.14.2

#!/usr/bin/perl use strict; use warnings; my $i; my @array; for $i ( 0..150 ) { $array[$i][0] = $i; $array[$i][1] = $array[$i-1][1] + 0.2; print "$array[$i][0]\t$array[$i][1]\n"; } exit;

which give me this output :

.... some lines 148 29.7999999999999 149 29.9999999999999 150 30.1999999999999
a strange thing is, if you make a loop for ( 0..100 ) there is no problems

have you seen this kind of thing ? is it a (known ?) bug ? or something else that i can fix in my code ?

Replies are listed 'Best First'.
Re: precision problem ?
by toolic (Bishop) on Aug 07, 2013 at 19:25 UTC
      .... yes it is ...

      thanks ^_^

      and as i have read (a long time ago) the long explanation, i think it is time (00:35) for me to have a sleeping break.
Re: precision problem ?
by Laurent_R (Canon) on Aug 07, 2013 at 21:14 UTC

    A classical example:

    $ perl -le 'print "Wrong " if 19.08 + 2.01 != 21.09;' Wrong

    You would most probably get the same type of result with Python, Ruby, C, Java, Fortran or what have you. The problem is not Perl, but the architecture of almost all current computers. The problem has to do with differences between numbers (the mathematical concept) and numerals (their representation on a computer). Just as 1/3 has a never ending 0.333333... representation in decimal notation, some numbers deemed to be simple in decimal notation have a never ending representation in the internal binary format of most computers.

    To find out the difference between 19.08 + 2.01 and 21.09, try this:

    $ perl -le 'print 21.09 - (19.08 + 2.01), "\n";' 3.5527136788005e-15

    3.5527136788005e-15 is admitedly a very small number, but it is not 0. The classical method for comparing floating point notation numbers is to check if the absolute value of their difference is smaller than a very small number usually called epsilon (perhaps something like 1e-14). If the difference is smaller than epsilon, then the number are considered to be the same. For example:

    $ perl -le '$epsilon = 1e-14; print "Wrong " if abs (19.08 + 2.01 - 21 +.09) > $epsilon;' $ perl -le '$epsilon = 1e-14; print "Wrong " if abs (19.08 + 2.02 - 21 +.09) > $epsilon;' Wrong

    In the first case, the difference is about 3e-15, so smaller than epsilon, I do not get an error; in the second case, the difference is about 0.01, much larger that epsilon, I get the "wrong" message.

    To tell the truth, there is another source of errors, which is perhaps more important in your case: the summation of rounding errors. A very classical problem: make an invoice of a few lines, with the tax free amount in the first column, and the tax-included amount in the second comumn, for a few items. Then calculate the total tax free amount, apply the common tax rate to that total mount, chances are high that the total tax-included amount calculated this way will not be the same as the sum of the tax-included items. The company for which I am a consultant spent several hundred thousand euros on an IT project to try to get rid of these rounding errors (making all calculations with 5 decimal digits), the situation is now much better, but you just can't get rid of all discrepancies, so that the invoice has a sentence somewhere saying that the tax amount for individual lines are only given as an estimate, only the last total line contains real amounts.

    BTW, using the bigint module seems to solve the problem:

    $ perl -le 'use bigint; print "Wrong " if 19.08 + 2.01 != 21.09;' $ perl -le 'use bigint; print 21.09 - (19.08 + 2.01), "\n";' 0

    I am not sure, though, that this works in all cases.

    Update: Oops, thanks choroba, the above (crossed part) is obviously silly. Yes, bignum should probably help much better. But I haven't tested yet.

      using the bigint module seems to solve the problem
      It rather seems to bring new problems:
      perl -le 'use bigint; print 21.09, " ", (19.08 + 2.01), "\n";' 21 21

      You probably meant bignum?

      لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ
        Of course, choroba, you are absolutely right. Silly mistake on my part. I am upvoting your post with my last vote for today.
Re: precision problem ?
by BillKSmith (Monsignor) on Aug 08, 2013 at 02:31 UTC
    It is probably "more than you care to know", but the full answer with all the mathamatical details can be found in http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html. Values for floating point constants applicable to your system can be found in the perl module POSIX. These details could be important if you ever really need the smallest possible epsilon for a given comparison.
    Bill
Re: precision problem ?
by Anonymous Monk on Aug 07, 2013 at 19:29 UTC
    i forgot to say that if you initialize $array[0][0] and array[0][1] at 0 and 0 and make a loop (1..150) the result is the same
Re: precision problem ?
by Anonymous Monk on Aug 08, 2013 at 10:41 UTC
    It's not a good idea to do addition or subtraction of decimal values (for the same variable) in a loop. (Well, not just those two operations -- anything floating-point in a loop.)

    If you really need to do that, you might want to refactor your code to use integer maths by e.g. multiplying the numbers by 10 (if the smallest number you use is 0.1) and then dividing by 10 when you're done with the maths (and need to use the numbers).

    my $i; my @array; for $i ( 0..150 ) { $array[$i][0] = $i; $array[$i][1] = $array[$i-1][1] + 2; printf "%s\t%s\n", $array[$i][0], $array[$i][1] / 10; }
Re: precision problem ?
by Anonymous Monk on Aug 08, 2013 at 20:45 UTC

    hello, the corrected code which seems to be the simple (for my needs)

    #!/usr/bin/perl use strict; use warnings; my $i; my @array; $array[0][0] = 0; $array[0][1] = 0; for $i ( 1..150 ) { $array[$i][0] = $i; $array[$i][1] = sprintf '%.6f', $array[$i-1][1] + 0.2; $array[$i][1] =~ s/\.*0*$//; print "$array[$i][0]\t$array[$i][1]\n"; } exit;

    the regexp is here to remove the unecessary precision

    thanks again for all yours answers