http://qs1969.pair.com?node_id=11151092

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

Mise Wonks,

The problem has been encountered many times: print int(2.26*100) = 225 (although print 2.26*100 = 226).

LanX's Humans have too many fingers warns about Floats are not accurate with decimal fractions because the computer "has only two fingers" and offers this advice: calculate with integers in the desired accuracy and shift the decimal point afterwards.

And this is what I was attempting to do: I was converting the float 2.26 into an integer 226 in order to increment it by 1. (Background: 2.26 is a version number and I wanted to bump it up to version 2.27. Since then I discovered Version::Next which can be a solution ...)

In C I get the correct result it seems to me:

# gcc a.c #include <stdio.h> #define ABS(A) ((A)>0?(A):(-(A))) int main(void){ float version = 2.26; float newversion = version + 0.01; printf("version=%f, newversion=%f\n", version, newversion); printf("2.26*100=%.8f\n", 2.26*100); float x = 226.0; if( ABS(x-226.0)>0.00000001 ){ printf("ajaja\n"); } }

And in bc: echo "2.26*100" | bc -l gives 226.00

I have perl v5.36.0 and a modern linux OS on a modern desktop, 64bit, and :

perl -V:nvtype -V:nvsize nvtype='double'; nvsize='8';

So, the advice to use decimal cents to do calculations in cents instead of using floating dollars is sound but how do I get to the decimal cents in the first place?

Oh, wonk: a person preoccupied with arcane details or procedures in a specialized field; broadly : nerd. hehe (Edit: I think "nerd" is superfluous and plain wrong).

bw, bliako

Replies are listed 'Best First'.
Re: Yet again: floats are not expressed accurately
by pryrt (Abbot) on Mar 21, 2023 at 14:36 UTC
    float x = 226.0; if( ABS(x-226.0)>0.00000001 ){ printf("ajaja\n"); }

    What you've shown here is 226.0 - 226.0, which may or may not be the same thing as 2.26*100.0 - 226.0; it would have been a more meaningful delta-check if you'd actually done the multiplication from the small float. However, when I tried the latter with the gcc that came with Strawberry, it still didn't show the "ajaja" message, so it's still closer than 0.00000001. That is because a c float is 32bit, so the 32bit ULP of 226.0 is 1.5259e-5. But you showed:

    perl -V:nvsize -V:nvtype nvsize='8'; nvtype='double';

    ... so perl is currently using a c double for NV... which means your c code should have been using doubles to replicate it. (You might ask "why did it work on previous versions?" A version of 2.25 doesn't have the rounding problem, because it's exactly representable in a c-double/perl-NV. And the version before that was 2.24, and 2.24*100=224.00000000000003 as a double, so int(that) rounds down to 224, so you just hadn't noticed it on that version.)

    The ULP_64bit_double(226.0) is 2.842e-14, so checking against 1e-8 is not sufficient. Updating the c-code to also show a double variable 2.26*100.0 and compare the error to ULP(226.0):

    /* gcc a.c */ #include <stdio.h> #define ABS(A) ((A)>0?(A):(-(A))) int main(void){ float version = 2.26; float newversion = version + 0.01; printf("version=%f, newversion=%f\n", version, newversion); /* bliako's "check" */ printf("2.26*100=%.8f\n", 2.26*100); float x = 226.0; if( ABS(x-226.0)>0.00000001 ){ printf("bliako's check has ABS erro +r\n"); } else {printf("bliako's check OK\n");} /* closer check: check the multiplied value against the hardcoded +226.0 */ /* as a 32bit float, the ULP(226.0) is (2**7)*(2**-23) = 2**-16 = + 15.259e-6 = 1.5259e-5, so a 1 ULP error would be >1.525e-5, so also +greater than 0.00000001 */ x = 2.26 * 100.0; printf("2.26*100=%.16f\n", x); if( ABS(x-226.0)>0.00000001 ){ printf("float-ULP check has ABS err +or\n"); } else {printf("float-ULP check OK\n");} /* but perl with nvtype=double requires a check against a c double +, where the ULP(226.0) = 2.842e-14 */ double d = 2.26 * 100.0; printf("2.26*100=%.16lf\n", d); if( ABS(d-226.0)>=2.842e-14 ){ printf("double-ULP check has ABS er +ror\n"); } else {printf("double-ULP check OK\n");} }

    gives output

    version=2.260000, newversion=2.270000 2.26*100=226.00000000 bliako's check OK 2.26*100=226.0000000000000000 float-ULP check OK 2.26*100=225.9999999999999700 double-ULP check has ABS error

    You will notice that the double shows the same issue as the perl v5.36.0 which you showed. (Please note, as I said in chatterbox yesterday, that this is dependent on perl's compilation choices (nvsize/nvtype), not on v5.36.0; the Strawberry Perl v5.32.1 that I use also has double nvtype, so it shows the same behavior with 2.26*100.0 that your v5.36.0 shows.

    So, the advice to use decimal cents to do calculations in cents instead of using floating dollars is sound but how do I get to the decimal cents in the first place?

    Like I suggested in the chatterbox yesterday, one possible way is perl -MPOSIX=round -le "printf(qq(%.2f), (round(2.26 * 100.0) + 1.0)/100.0)" -- this uses POSIX's round() function to round-to-nearest after you've done the multiplication. Or, to put it another way, my $cents = POSIX::round(2.26*100);, then do the remaining calculations with the cents.

    Or treat the number as a string, and, as syphilis implied, split the string on the decimal point, and just do the cents manipulation on the righthand side, after error checking and appropriate manipulation, depending on whether you mean `1.1` to be 1*100+1 or 1*100+10, and whether you want to carry from 1.99 to 2.00 or not.

      Thank you pryrt. One reason to pursue this further (i.e. bothering all of you here) was that C was not showing it, but as you showed, my mistake! was not to use double precision like my perl does. I have used your POSIX::round suggestion yesterday and it did work. Also, with Perl's easy numerical <-> string conversions it's easy to go to cents by a string-manipulation route (remove and add the dot) and avoid even getting the cents wrong as was in my case. So, yes thanks and thanks to all for their input and suggestions.

        A unique position where it both makes cents, and doesn't make scents.... :-)
Re: Yet again: floats are not expressed accurately
by LanX (Saint) on Mar 21, 2023 at 11:23 UTC
    TIMTOWTDI
    DB<13> sub inc_version { my ($ver,$pos) = @_ ; my @vers = split /\./ +,$ver; $vers[$pos]++; return join '.', @vers } DB<14> p inc_version("2.26.3",$_),"\n" for 0..2 3.26.3 2.27.3 2.26.4

    tho you might want to consider sprintf to format to 3 digits

    edit

    ehm ...

    > newversion = version + 0.01;

    .... so 2.99 becomes 3.00 ? I hope it's clear now why you shouldn't use floats...

    Cheers Rolf
    (addicted to the 𐍀𐌴𐍂𐌻 Programming Language :)
    Wikisyntax for the Monastery

      I hope it's clear now why you shouldn't use floats...

      Yes - version numbers are actually integers separated by dots. To increment, just ++ the final integer.

      Cheers,
      Rob

      granted! (for versions).I just went to the bank because I needed 226 drachmas but decided to draw 2.26 at a time... as these are thought experiments I am a at a loss as to how much i have in my pocket and how much still in my account...

        Don't be so drachmatic!!! ;P

        Some languages (raku?) are trying to solve the issue by having a number type for fractions°. Hence integer operations behind the scenes.

        And that's what you should do with your West Asian island coins.

        Add them as integers and move the point afterwards.

        Sorry, we had this discussion already so often that I'm not too motivated to further discuss it.˛

        FWIW

        °) there was a pragma to turn scalars into slow objects doing just this.

        use fract or something...

        Updates

        https://perldoc.perl.org/bigrat but it doesn't seem to support dot-notation...

        ˛) See https://floating-point-gui.de/ for more.

        Cheers Rolf
        (addicted to the 𐍀𐌴𐍂𐌻 Programming Language :)
        Wikisyntax for the Monastery

Re: Yet again: floats are not expressed accurately
by NERDVANA (Deacon) on Mar 23, 2023 at 00:33 UTC
    but how do I get to the decimal cents in the first place?

    Assuming you start with a string, you have to avoid triggering perl's implied parse to a floating point number.

    my $input= "2.26"; my ($whole, $frac)= split /\./, $input; $frac //= ''; length $frac < 2 or die "would lose precision on .$frac"; $frac .= '0'x(2 - length($frac)); my $cents= "$whole$frac";

    Yes, it's ugly. I'm sure there is some clever way to do this with a regex that runs faster, and would be even uglier. But, you can just stick that in a function named "parse_cents" or "parse_decimal($str,$scale)" and hide the details.

    Another option is to use Math::BigRat, which will handle all arbitrary cases, but then the performance of all the math you do on the numbers will be a little slower.

Re: Yet again: floats are not expressed accurately
by tybalt89 (Monsignor) on Mar 23, 2023 at 02:52 UTC
    $ perl -le 'for (2.25, 2.26, 2.27){print int +($_ + 0.005) * 100}' 225 226 227