Beefy Boxes and Bandwidth Generously Provided by pair Networks
Do you know where your variables are?

Parsing Math Strings

by pokemonk (Scribe)
on Dec 06, 2001 at 20:11 UTC ( [id://129948] : perlquestion . print w/replies, xml ) Need Help??

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

I'm working on a simple graphing cgi, but i don't know how to parse the function (y=23^x or y=2x+3 etc). so if i have $string = '(1/2)*4'; how do i write a function that returns 2 when i send it that string? Is there a module? i hope i'm clear, thanks a lot guys.


Replies are listed 'Best First'.
Re: Parsing Math Strings
by Masem (Monsignor) on Dec 06, 2001 at 20:26 UTC
    You could go one of two ways: The first is to use Parse::RecDescent to create a math grammar to develop an expression tree that you can evaluate numerous ways. However, this is probably the more difficult way to go.

    The better solution is to selectively use eval. Set up a hash that will store variable values. Use a regex to translate 'bare' variables in your expressions to point to this hash. Then eval the expression as necessary:

    my $exp = "y=2*x+3"; my %vars = ( x => 1 ); $exp =~ s/([a-z]+)/\$vars{\1}/g; eval $exp; print $vars{ y };

    Dr. Michael K. Neylon - || "You've left the lens cap of your mind on again, Pinky" - The Brain
    "I can see my house from here!"
    It's not what you know, but knowing how to find it if you don't know that's important

      If you're not up to the Parse::RecDescent challenge, you can download the code used in chapter 6 ("Global Matching") of my book. Specifically, from 6.6, "Parsing a language". The code is in examples 6.6 and 6.7. It implements a simple calculator with variable support.

      Jeff[japhy]Pinyan: Perl, regex, and perl hacker.
      s++=END;++y(;-P)}y js++=;shajsj<++y(p-q)}?print:??;

      Another possibility is Math::Expr.

      After Compline,

Re: Parsing Math Strings
by lhoward (Vicar) on Dec 06, 2001 at 20:27 UTC
    Several people have recomended using eval to solve this problem. Though eval will work, it can be very dangerous in situations where you do not have control over the data passed in to the eval (such as in the case of a cgi script). Consider if you were using evel and someone were to pass in system("rm -rf *") to your CGI program instead of the math expression you were expecting?
      So the solution would be:
      #You might want to do something to look for say sin, cos, tan, etc. unless( $str =~ /[^0-9+-*^/.,_()]/ ){ eval($str); }

      perl -p -e "s/(?:\w);([st])/'\$1/mg"

        Or use the Safe module and mask the operators that can be considered potentially harmful. This has the benefit of allowing the "math expression" to be arbitrarily complex (Perl, really), and if you pre-loaded things like Math::Complex, you could potentially have a very powerful "calculator."
Re: Parsing Math Strings
by tune (Curate) on Dec 06, 2001 at 20:19 UTC
(tye)Re: Parsing Math Strings
by tye (Sage) on Dec 07, 2001 at 02:03 UTC

    One issue that I didn't see touched on was execution speed. It appears that you'd want to calculate, for example, (X/2)^4 for quite a few values of X. The fastest way to do this would be to create a subroutine that does the calculation. For example, a closure:

    my $sub= eval "sub { my($x)=@_; return ($x/2)**4 }"; die $@ if $@; for my $x ( 0..100 ) { $x /= 10; my $y= $sub->( $x ); plot( $x, $y ); }
    So I'd probably validate the input and use eval to build an anonymous subroutine similar to what I show above.

    So the trick is deciding what to allow that will be safe.

    If you only allow digits and mathish punctuation, then I can't come up with a way that the result would be dangerous:

    if( $equ =~ m#[^-\s\d+*/()]# ) { # refuse to use it
    You could even allow a specific list of variable names and functions:
    my $equ= $q->param("equation"); my( $var, $expr )= $equ =~ /^\s*(\w+)\s*=(.*)/ or fail("Invalid equation"); my @words= qw( x y z abs atan2 cos exp log sin sqrt ); my %words; @words{@words}= (" ") x @words; ( my $check= $expr ) =~ s/(\w+)/ $words{$1} ? "" : "x" /ge; if( $check =~ m#([^-\s\d+*/()]+)# ) { fail( "Disallowed function/variable ($1)" ); } @words{qw(x y z)}= qw( $x $y $z ); $expr =~ s/(\w+)/ $words{$1} /ge; my $sub= eval 'sub { my($x,$y,$z)= @_; return ' . $expr . '}'; fail( "Invalid expression ($expr): $@" ) if $@;

            - tye (but my friends call me "Tye")
Re: Parsing Math Strings
by Fletch (Bishop) on Dec 06, 2001 at 20:44 UTC

    You could just punt and pass things off to gnuplot.

Re: Parsing Math Strings
by Fastolfe (Vicar) on Dec 06, 2001 at 20:19 UTC