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

Hello Dear Monks, I need your wise and wisdom once more. I am reading data from a web page, some of the contents are fractional numbers. After reading them I have to do some aritmetic with these fractions. That's where the problem starts: I get "Argument "6/2" isn't numeric in addition (+) ......." How can I do math with fractions? Thanks in advance for your help?

Replies are listed 'Best First'.
Re: Arithmetic with fractions
by ikegami (Patriarch) on Sep 22, 2004 at 06:19 UTC

    There's a module (I never used) called Math::Fraction which seems to do the trick. It even handles mixed fractions:

    use Math::Fraction; $a = frac("10 3/4"); $b = frac("1/2"); $c = frac("2/5"); $d = $a * $b + $c; print("$d\n");

    It supports:

    + - / * + += -= *= /= ++ -- abs <=> == != < <= > >= ** sqrt

    and has some useful conversion methods.

      I fed Math::Fractions a decimal value of 23.7800 which it interpreted as 214/9 which in decimal value is 23.777777777777777777777777777778. I couldn't figure out if I should *not* be using the frac() method on a decimal value or whether or not the module is buggy. Can you?
Re: Arithmetic with fractions
by tachyon (Chancellor) on Sep 22, 2004 at 05:32 UTC

    You need to evaluate the string "6/2" to get the numerical value 3 before you try to do the addition. You can do it using eval but eval on strings is dangerous as it will *execute* the code in the string. This function should be safe to use as it will only do the eval if the string contains + - . 0 1 2 3 4 5 6 7 8 9 / plus spaces and tabs which are harmless. These chars should be all you need for the task at hand....

    sub frac2dec { my $str = shift; $str = eval $str if $str =~ m!^[\-\+\d\./ \t]+$!; $str; } $a = " 6/2"; $b = " 4 / 8 "; print frac2dec($a) + frac2dec($b); __DATA__ 3.5

    cheers

    tachyon

      This function should be safe to use as it will only do the eval if the string contains + - . 0 1 2 3 4 5 6 7 8 9 / plus spaces and tabs which are harmless.

      It is safer than an unchecked eval, but not completely safe or devoid of side effects. An attacker could:

      a) Cause the eval to throw an exception with input like 5++ or 4/0. This could in turn make the whole program die or misbehave in certain situation, and has the side effects of triggering the signal handlers and clobbering $@, in addition to making the eval return undef.

      b) Test certain numeric patterns against $_ or even compute its value. For example, the following input makes the eval return the value of $_, if it's an integer in the range 0..99.

      /1//1+/2//0.5+/3//0.333333333333333+/4//0.25+/5//0.2+/6// 0.166666666666667+/7//0.142857142857143+/8//0.125+/9// 0.111111111111111+/1.//0.1-/1.//1+/2.//0.05-/2.//0.5+/3.// 0.0333333333333333-/3.//0.333333333333333+/4.//0.025-/4.// 0.25+/5.//0.02-/5.//0.2+/6.//0.0166666666666667-/6.// 0.166666666666667+/7.//0.0142857142857143-/7.// 0.142857142857143+/8.//0.0125-/8.//0.125+/9.// 0.0111111111111111-/9.//0.111111111111111+/11//1+/22// 0.5+/33//0.333333333333333+/44//0.25+/55//0.2+/66// 0.166666666666667+/77//0.142857142857143+/88//0.125+/ 99//0.111111111111111

      c) I DON'T KNOW if it's possible to create a regexp that takes forever to run only with the allowed characters.

        a) eval does not throw exceptions. (Quite the opposite.) Given those strings, undef will be returned, which sounds perfectly acceptable. It should also return undef when validation fails, but it doesn't.

        b) While this function returning a number in 0..99 doesn't sound dangerous, I have to figure how this happens.

        c) Shouldn't you be asking: Is it possible to make THAT regexp take forever? After all, the user isn't providing the regexp. If that simple regexp can take forever, Perl is in big trouble, not just this program.

      Excellent, thanks a millon.
      Thanks a lot tachyon, it worked perfectly. My fracs will never have minus signs in front, so it should always work without a problem. However I have one more question, I know this is a very silly one : ) Once I come accross a fraction or an integer with a minus sign in front, I have to be able to recognize and act upon it. How would my if statement be then? I am sure the regex for it must be a very easy one but I just don't know enough perl to do it. Any help would be appreciated. Thanks.
Re: Arithmetic with fractions
by ysth (Canon) on Sep 22, 2004 at 07:22 UTC
    To convert the fractions into floating point numbers that perl can directly deal with, you need to split them up. Some ways:
    $num = "6/2"; # like this $num =~ s:^(\d+)/(\d+)\z:$1/$2:e; # or my ($n,$d) = split "/", $num; $num = $n/$d; # or (unsafe for untrusted input) $num = eval $num;
    But that isn't sufficient if you need to preserve the fractional values. Floating point will introduce some round-off error. For instance, adding 1/6 six times may not get you 1:
    $ perl -we'print "not equal" if 1/6 + 1/6 + 1/6 + 1/6 + 1/6 != 1' not equal
    Math::BigRat (which I've never actually used) is a core module to handle this for you.
    $ perl -we'use Math::BigRat; $onesixth = Math::BigRat->new("1/6"); > print "equal" if $onesixth+$onesixth+$onesixth+$onesixth+$onesixth+ > $onesixth == 1' equal
Re: Arithmetic with fractions
by johnnywang (Priest) on Sep 22, 2004 at 05:26 UTC
    You can parse it into numerator/denomenator:
    my $s = "6/2"; my($num,$de)= split /\//,$s;
    or just eval:
    my $s = "6/2"; 4 + eval($s); # 7
      $s = 'rm -rf / && echo "Un-checked string evals are bad"'
Re: Arithmetic with fractions
by TedPride (Priest) on Sep 22, 2004 at 06:47 UTC
    $fraction = '32 2/5'; &fconv(\$fraction); sub fconv { my $f = shift; if ($$f =~ /((\d+) )?(\d+) ?\/ ?(\d+)/) { $$f = $2 + $3/$4; } }
    This will convert a fraction or mixed number of any form to a decimal equivalent. The following are some possibilities:

    2/5
    2 / 5
    2 /5
    32 2/5
    32 2 / 5

      Why are you capturing to $1? You don't deal with signs, so how about:

      sub fconv { my $f = shift; if ($$f =~ m!([\-\+]*)\s*(?:(\d+)\s+)?(\d+)\s*/\s*(\d+)!) { $$f = $2 + $3/$4; $$f = -$$f if $1 eq '-'; } }

      cheers

      tachyon

        $fraction = '-32 2/5'; &fconv(\$fraction); sub fconv { my $f = shift; if ($$f =~ /(-?) ?((\d+) )?(\d+) ?\/ ?(\d+)/) { $$f = ($1 eq '-' ? -1 : 1) * ($3 + $4/$5); } }
        There, it now works with negatives. My bad on the earlier version :)

        As for Perl's problems with number management, that can't really be helped. You can always use the fractions class mentioned above, or round your results to the nearest thousandth, which should produce the same results.