http://qs321.pair.com?node_id=11123116

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

Hi Monks

I came across with a strange result when I'm using int() function. For example,

my $x1 = (1.15*170)+0.50; my $x2 = int($x1); print "Number is: $x1, Integer part is: $x2";

It prints "Number is: 196, Integer part is: 195", but I would expect $x2 to be 196

But when I am using different numbers I am getting the correct results. For example,

my $x1 = (1.15*150)+0.50; my $x2 = int($x1); print "\nNumber is: $x1, Integer part is: $x2";
my $x1 = (1.15*30)+0.50; my $x2 = int($x1); print "\nNumber is: $x1, Integer part is: $x2";

Can anybody guess what goes wrong with the first one, what goes wrong when I'm using the number 170? At least, I would expect either all results be correct or all results be wrong, but not just one be wrong.

Using int(sprintf("%.2f",$x1)) fixes the error but this is not the issue.

Replies are listed 'Best First'.
Re: int() function
by syphilis (Archbishop) on Oct 24, 2020 at 13:34 UTC
    It prints "Number is: 196, Integer part is: 195", but I would expect $x2 to be 196

    Your expectation is unreasonable. (I know ... it seems like a reasonable expectation ... but it is not.)

    First of all, note that (1.15*170)+0.50 cannot be exactly represented in a double.
    This is mainly due to the fact that 1.15 cannot be exactly represented in a double.
    When you assign a value of 1.15 to a double you are actually assigning the precise value 1.149999999999999911182158029987476766109466552734375.
    And the expression (1.15*150)+0.50 evaluates to exactly 195.999999999999971578290569595992565155029296875.

    Perl's print() function takes the action of rounding that value to 15 decimal digits. It does that for all floating point values - always to 15 decimal digits (unless perl's nvtype is other than "double") and always for a dubious reason.

    So ... quite clearly, the integer portion of 195.999999999999971578290569595992565155029296875 is 195, and 195.999999999999971578290569595992565155029296875 rounded to 15 decimal places is 196.
    And this agrees with the results that you obtained.

    It leaves shaking my head in disbelief, too ... but that's the stupid way that perl's print() function has been doing it for a long time and, unfortunately, I can't see it doing anything differently any time soon.
    A far saner approach would be for perl's print() function to have displayed the value 195.99999999999997 as happens with python3 and raku.
    Note that, although you have questioned the printed value of $x2, it's actually the printed value of $x1 that's misleading.

    Cheers,
    Rob

      Minor correction: in OP's code, it's not Perl's print() function that is rounding the floating point value up to 196. It's happening on string interpolation.

        I think it's anything which does a stringification.

        DB<137> $x1 = (1.15*170)+0.50; DB<138> p "$x1" 196 DB<139> p $x1 196 DB<140> printf "%.20f\n",$x1 195.99999999999997000000 DB<141> $str = $x1."" DB<142> p length $str 3 DB<143>

        yep!

        DB<151> $x2 = (1.15*170)+0.50; DB<152> Dump $x2 SV = NV(0x32cc698) at 0x32cc6b0 REFCNT = 1 FLAGS = (NOK,pNOK) NV = 196 DB<153> p $x2 196 DB<154> Dump $x2 SV = PVNV(0x1cb938) at 0x32cc6b0 REFCNT = 1 FLAGS = (NOK,pNOK) IV = 0 NV = 196 PV = 0x31a4748 "196"\0 CUR = 3 LEN = 32

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        Wikisyntax for the Monastery

      > It does that for all floating point values - always to 15 decimal digits (unless perl's nvtype is other than "double") and always for a dubious reason.

      The mantissa of a double has 52 bits°, you need 50 bits to encode 15 decimal digits ( 2**10 =1024, so rule of thumb 10 bits for 3 decimals)

      So the last two bits are used for error correction, since they don't suffice to encode another decimal digit.

      Is it a good solution? Well I'd say appropriate for the era and not "for a dubious reason".

      Are there better approaches? Sure, but they are harder to implement and would have been quite slow back in the days.

      From my experience, >95% of the problems arise from calculating with currencies and the solution is obvious, calculate with integer cents and move the point only for output.

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

      °) WP actually says 53 by using a spare bit redundancy , but tl;dr and this still fails to encode 16 decimals.

        Well I'd say appropriate for the era and not "for a dubious reason".

        I probably should have said "for lack of a sound reason". (Hmmm ... not sure if that's any different ;-)

        It's just that, if the stringification provided an extra 2 decimal digits of precision, we would avoid having to look at rubbish diagnoses like this one (where the test fails but "got" and "expected" are reported as being the same):
        C:\_32>perl -MTest::More -le "cmp_ok(0.14, '==', 1.4 / 10, '0.14 == 1. +4/10'); done _testing();" not ok 1 - 0.14 == 1.4/10 # Failed test '0.14 == 0.14' # at -e line 1. # got: 0.14 # expected: 0.14 1..1 # Looks like you failed 1 test of 1.
        IMO, if $x is an NV, then the condition "$x" == $x should always be true unless $x is NaN.
        And that's the way it would be if doubles stringified to 17 digits of precision instead of the current 15 digits.
        I would regard that as being a significant improvement for very little cost.
        And we would then see that "got" is 0.14000000000000001 and "expected" is 0.13999999999999999 - which at least makes some sense.

        It's still not ideal because the strings "0.14000000000000001" and "0.14" both assign to the same double - so why print out all of those extra digits ?
        Python3 (and Raku, I believe) use as few digits as are needed and would report the double 0.14 as being "0.14" and not "0.14000000000000001".

        I've implemented that Python3/Raku behaviour in Math::MPFR - though with a different algorithm and probably not as efficiently as Python3/Raku.
        C:\>perl -MMath::MPFR="nvtoa" -le "print nvtoa(0.14);print nvtoa(1.4 / + 10);" 0.14 0.13999999999999999
        Cheers,
        Rob

        It has 53 bits of precision because there's an implied leading 1 bit that's not stored.

        log10(2^53) ~ 15.95, not quite 16. So not exactly 16 decimal digits of precision, but damn close. What's important here is that it's more than 15.

        $ perl -e' printf "%.16f\n", 0.1234567890123456; printf "%.16f\n", 0.1234567890123457; ' 0.1234567890123456 0.1234567890123457

      Thank you Rob

      You are right. In my first example (1.15*170)+.5 the 15th decinal digit is 4 so it rounds to 195, whereas in my second example (1.15*150)+.5 the 15th decinal digit is 6 so it rounds to 173.

      The 1.15 is actually 15% (selectable by user) on top of a price. So the script calculates (1+ 15/100)*price and because the final price must be rounded to the nearest integer, I added 0.50

      I wanted to avoid loading Math::Round for just one calculation.

        > I wanted to avoid loading Math::Round for just one calculation.

        Did you see my reply?

        Especially the last part titled "workaround"? ;)

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        Wikisyntax for the Monastery

Re: int() function
by haukex (Archbishop) on Oct 24, 2020 at 22:47 UTC

    Since noone has linked to it yet: What Every Programmer Should Know About Floating-Point Arithmetic

    Note that if you don't care about taking a performance hit (which can be quite significant depending on how many variables and calculations you're using in your actual script, so make sure to benchmark), you can enable bignum. Note that it can also be enabled only for smaller lexical scopes, which is what I'm showing here:

    my $x1 = (1.15*170)+0.50; my $x2 = int($x1); print "A: Number is: $x1, Integer part is: $x2\n"; { use bignum; my $x3 = (1.15*170)+0.50; my $x4 = int($x3); print "B: Number is: $x3, Integer part is: $x4\n"; } __END__ A: Number is: 196, Integer part is: 195 B: Number is: 196, Integer part is: 196
Re: int() function (updated)
by LanX (Saint) on Oct 24, 2020 at 11:31 UTC
    Perl does an internal magic error correction/guessing when printing a number like 195.999999...etc to compensate rounding errors on the last digits. (This of course in binary and not decimal representation)

    I think that's why it prints $x1 as 196.

    I'll check as soon my laptop is up and running.

    update

    as I thought, it's compensating the rounding error from not being able to accurately represent 0.15 in a based 2 floating number:

    DB<28> $x1 = (1.15*170)+0.50; DB<29> printf("%.20f",$x1) 195.99999999999997000000 DB<30> printf("%.20f",0.15) 0.14999999999999999000 DB<31>

    There is an easy workaround , avoid the decimal point when calculating and divide at the very end.

    DB<35> $x1 = (115*170)+50; # no fractions DB<36> $x1 /=100 DB<37> p "$x1 = ",int $x1 196 = 196 DB<38>

    see also Humans have too many fingers

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

Re: int() function
by holli (Abbot) on Oct 24, 2020 at 13:47 UTC
    C:\>rakudo -e "say (1.15*170)+0.5" 196
    #MathThatWorks #Rakulang


    holli

    You can lead your users to water, but alas, you cannot drown them.
      C:\>rakudo -e "say (1.15*170)+0.5" 196


      That's interesting - I thought that raku was above producing that sort of braindead bullshit.
      Is it rounding to the nearest integer ?
      Or maybe it's doing decimal math ?

      UPDATE: aaah ... I think I get it ... 1.15 is being evaluated as the rational value 115/100 ... right ?

      Cheers,
      Rob
        Yes, Raku has a Rat type which stores (and normalizes) numerator and denominator. Legend has it they only did that so they can have a .nude method (which returns a Pair).


        holli

        You can lead your users to water, but alas, you cannot drown them.
Re: int() function
by haj (Vicar) on Oct 24, 2020 at 11:09 UTC

    Edit: I realise now that this is not a good answer to your question, so scratch that. I need to go now, so can't fix it in the next minutes - sorry for the noise.

    The issue is, as ever so often, limited accuracy of floating point calculations. The number 1.15 can not be represented exactly as a floating point number, so the representation is either a tiny bit below, or a tiny bit above 1.15.

    Apparently it is a tiny bit below 1.15, so the result of the multiplication is a tiny bit below 195.5, and after the addition of 0.5 you get something a tiny bit below 196.

    The int function doesn't care about how tiny that bit might be, it happily throws away everything below 196 because, well, it is less than 196.

    There's nothing magical about the number 170: The tiny error is either negative or positive, and whenever it is negative, int will behave like that.

    So, using printf is a well accepted way to get around this issue, because it gets the rounding right for normal expectations.

Re: int() function
by karlgoethebier (Abbot) on Oct 28, 2020 at 20:18 UTC

    You may also see GMP and consider use bignum lib => 'GMP'; as you are in a hurry.

    «The Crux of the Biscuit is the Apostrophe»

    perl -MCrypt::CBC -E 'say Crypt::CBC->new(-key=>'kgb',-cipher=>"Blowfish")->decrypt_hex($ENV{KARL});'Help

Re: int() function
by ikegami (Patriarch) on Oct 27, 2020 at 10:43 UTC

    15/100 is periodic number in binary (0.00 1001 1001 1001 ...) just like 1/3 is periodic in decimal (0.333...) . As such, it can't accurately be stored as a floating point number since that would take infinite storage. The number you think you have isn't quite what you think it is.

    $ perl -e'CORE::say sprintf("%.100f", 0.15) =~ s/0+\z//r' 0.1499999999999999944488848768742172978818416595458984375 $ perl -e'CORE::say sprintf("%.100f", (1.15*170)+0.50) =~ s/0+\z//r' 195.999999999999971578290569595992565155029296875
    (Upd: syphilis points out %.100g naturally remove the trailing zeros.)

    See What Every Programmer Should Know About Floating-Point Arithmetic. It covers how to handle this.

      Not that it's of great importance, but is there a reason that you've chosen to use "%.100f" formatting instead of "%.100g" formatting ?
      The "%.100g" formatting should remove any trailing zeros, thereby alleviating the need for the regex.

      I know that, on some systems, %g formatting can be buggy, but I don't know of any system where "%.100g" misbehaves && "%.100f" functions correctly.

      Cheers,
      Rob
        On my CentOS 7 box with perl-5.16.3
        $ perl -E'say sprintf("%.100g", 0.15)' 0.14999999999999999 $ perl -E'say sprintf("%.100f", 0.15)' 0.14999999999999999444888487687421729788184165954589843750000000000000 +00000000000000000000000000000000 $ perl --version | head -3 This is perl 5, version 16, subversion 3 (v5.16.3) built for x86_64-li +nux-thread-multi (with 40 registered patches, see perl -V for more detail)
        On my Fedora 32 box with perl-5.30.3
        $ perl -E'say sprintf("%.100g", 0.15)' 0.1499999999999999944488848768742172978818416595458984375 $ perl -E'say sprintf("%.100f", 0.15)' 0.14999999999999999444888487687421729788184165954589843750000000000000 +00000000000000000000000000000000 $ perl -E'say sprintf("%.100a", 0.15)' 0x1.333333333333300000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000p-3 $ perl -v|head -3 This is perl 5, version 30, subversion 3 (v5.30.3) built for x86_64-li +nux-thread-multi (with 96 registered patches, see perl -V for more detail)

        No, it's simply an oversight.