• Main Page
  • Related Pages
  • Modules
  • Namespaces
  • Data Structures
  • Files
  • File List
  • Globals

Services/Math/classes/class.EvalMath.php

Go to the documentation of this file.
00001 <?php
00002 
00003 /*
00004 ================================================================================
00005 
00006 EvalMath - PHP Class to safely evaluate math expressions
00007 Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
00008 
00009 ================================================================================
00010 
00011 NAME
00012     EvalMath - safely evaluate math expressions
00013     
00014 SYNOPSIS
00015     <?
00016       include('evalmath.class.php');
00017       $m = new EvalMath;
00018       // basic evaluation:
00019       $result = $m->evaluate('2+2');
00020       // supports: order of operation; parentheses; negation; built-in functions
00021       $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
00022       // create your own variables
00023       $m->evaluate('a = e^(ln(pi))');
00024       // or functions
00025       $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
00026       // and then use them
00027       $result = $m->evaluate('3*f(42,a)');
00028     ?>
00029       
00030 DESCRIPTION
00031     Use the EvalMath class when you want to evaluate mathematical expressions 
00032     from untrusted sources.  You can define your own variables and functions,
00033     which are stored in the object.  Try it, it's fun!
00034 
00035 METHODS
00036     $m->evalute($expr)
00037         Evaluates the expression and returns the result.  If an error occurs,
00038         prints a warning and returns false.  If $expr is a function assignment,
00039         returns true on success.
00040     
00041     $m->e($expr)
00042         A synonym for $m->evaluate().
00043     
00044     $m->vars()
00045         Returns an associative array of all user-defined variables and values.
00046         
00047     $m->funcs()
00048         Returns an array of all user-defined functions.
00049 
00050 PARAMETERS
00051     $m->suppress_errors
00052         Set to true to turn off warnings when evaluating expressions
00053 
00054     $m->last_error
00055         If the last evaluation failed, contains a string describing the error.
00056         (Useful when suppress_errors is on).
00057 
00058 AUTHOR INFORMATION
00059     Copyright 2005, Miles Kaufmann.
00060 
00061 LICENSE
00062     Redistribution and use in source and binary forms, with or without
00063     modification, are permitted provided that the following conditions are
00064     met:
00065     
00066     1   Redistributions of source code must retain the above copyright
00067         notice, this list of conditions and the following disclaimer.
00068     2.  Redistributions in binary form must reproduce the above copyright
00069         notice, this list of conditions and the following disclaimer in the
00070         documentation and/or other materials provided with the distribution.
00071     3.  The name of the author may not be used to endorse or promote
00072         products derived from this software without specific prior written
00073         permission.
00074     
00075     THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
00076     IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00077     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00078     DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
00079     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
00080     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
00081     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
00082     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
00083     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00084     ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00085     POSSIBILITY OF SUCH DAMAGE.
00086 
00087 */
00088 
00089 class EvalMath {
00090 
00091     var $suppress_errors = false;
00092     var $last_error = null;
00093     
00094     var $v = array('e'=>2.71,'pi'=>3.14); // variables (and constants)
00095     var $f = array(); // user-defined functions
00096     var $vb = array('e', 'pi'); // constants
00097     var $fb = array(  // built-in functions
00098         'sin','sinh','arcsin','asin','arcsinh','asinh',
00099         'cos','cosh','arccos','acos','arccosh','acosh',
00100         'tan','tanh','arctan','atan','arctanh','atanh',
00101         'sqrt','abs','ln','log');
00102     
00103     function EvalMath() {
00104         // make the variables a little more accurate
00105         $this->v['pi'] = pi();
00106         $this->v['e'] = exp(1);
00107     }
00108     
00109     function e($expr) {
00110         return $this->evaluate($expr);
00111     }
00112     
00113     function evaluate($expr) {
00114         $this->last_error = null;
00115         $expr = trim($expr);
00116         if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
00117         //===============
00118         // is it a variable assignment?
00119         if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
00120             if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
00121                 return $this->trigger("cannot assign to constant '$matches[1]'");
00122             }
00123             if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
00124             $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
00125             return $this->v[$matches[1]]; // and return the resulting value
00126         //===============
00127         // is it a function assignment?
00128         } elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
00129             $fnn = $matches[1]; // get the function name
00130             if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
00131                 return $this->trigger("cannot redefine built-in function '$matches[1]()'");
00132             }
00133             $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
00134             if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
00135             for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
00136                 $token = $stack[$i];
00137                 if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
00138                     if (array_key_exists($token, $this->v)) {
00139                         $stack[$i] = $this->v[$token];
00140                     } else {
00141                         return $this->trigger("undefined variable '$token' in function definition");
00142                     }
00143                 }
00144             }
00145             $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
00146             return true;
00147         //===============
00148         } else {
00149             return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
00150         }
00151     }
00152     
00153     function vars() {
00154         $output = $this->v;
00155         unset($output['pi']);
00156         unset($output['e']);
00157         return $output;
00158     }
00159     
00160     function funcs() {
00161         $output = array();
00162         foreach ($this->f as $fnn=>$dat)
00163             $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
00164         return $output;
00165     }
00166 
00167     //===================== HERE BE INTERNAL METHODS ====================\\
00168 
00169     // Convert infix to postfix notation
00170     function nfx($expr) {
00171     
00172         $index = 0;
00173         $stack = new EvalMathStack;
00174         $output = array(); // postfix form of expression, to be passed to pfx()
00175         $expr = trim(strtolower($expr));
00176         
00177         $ops   = array('+', '-', '*', '/', '^', '_');
00178         $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?  
00179         $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
00180         
00181         $expecting_op = false; // we use this in syntax-checking the expression
00182                                // and determining when a - is a negation
00183     
00184         if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
00185             return $this->trigger("illegal character '{$matches[0]}'");
00186         }
00187     
00188         while(1) { // 1 Infinite Loop ;)
00189             $op = substr($expr, $index, 1); // get the first character at the current index
00190             // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
00191             $ex = preg_match('/^([01]+[bB]|[\da-fA-F]+[hH]|[a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
00192             //===============
00193             if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
00194                 $stack->push('_'); // put a negation on the stack
00195                 $index++;
00196             } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack 
00197                 return $this->trigger("illegal character '_'"); // but not in the input expression
00198             //===============
00199             } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
00200                 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
00201                     $op = '*'; $index--; // it's an implicit multiplication
00202                 }
00203                 // heart of the algorithm:
00204                 while($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
00205                     $output[] = $stack->pop(); // pop stuff off the stack into the output
00206                 }
00207                 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
00208                 $stack->push($op); // finally put OUR operator onto the stack
00209                 $index++;
00210                 $expecting_op = false;
00211             //===============
00212             } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
00213                 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
00214                     if (is_null($o2)) return $this->trigger("unexpected ')'");
00215                     else $output[] = $o2;
00216                 }
00217                 if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function?
00218                     $fnn = $matches[1]; // get the function name
00219                     $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
00220                     $output[] = $stack->pop(); // pop the function and push onto the output
00221                     if (in_array($fnn, $this->fb)) { // check the argument count
00222                         if($arg_count > 1)
00223                             return $this->trigger("too many arguments ($arg_count given, 1 expected)");
00224                     } elseif (array_key_exists($fnn, $this->f)) {
00225                         if ($arg_count != count($this->f[$fnn]['args']))
00226                             return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)");
00227                     } else { // did we somehow push a non-function on the stack? this should never happen
00228                         return $this->trigger("internal error");
00229                     }
00230                 }
00231                 $index++;
00232             //===============
00233             } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
00234                 while (($o2 = $stack->pop()) != '(') { 
00235                     if (is_null($o2)) return $this->trigger("unexpected ','"); // oops, never had a (
00236                     else $output[] = $o2; // pop the argument expression stuff and push onto the output
00237                 }
00238                 // make sure there was a function
00239                 if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches))
00240                     return $this->trigger("unexpected ','");
00241                 $stack->push($stack->pop()+1); // increment the argument count
00242                 $stack->push('('); // put the ( back on, we'll need to pop back to it again
00243                 $index++;
00244                 $expecting_op = false;
00245             //===============
00246             } elseif ($op == '(' and !$expecting_op) {
00247                 $stack->push('('); // that was easy
00248                 $index++;
00249                 $allow_neg = true;
00250             //===============
00251             } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
00252                 $expecting_op = true;
00253                 $val = $match[1];
00254                 if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
00255                     if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
00256                         $stack->push($val);
00257                         $stack->push(1);
00258                         $stack->push('(');
00259                         $expecting_op = false;
00260                     } else { // it's a var w/ implicit multiplication
00261                         $val = $matches[1];
00262                         $output[] = $val;
00263                     }
00264                 } else { // it's a plain old var or num
00265                     $output[] = $val;
00266                 }
00267                 $index += strlen($val);
00268             //===============
00269             } elseif ($op == ')') { // miscellaneous error checking
00270                 return $this->trigger("unexpected ')'");
00271             } elseif (in_array($op, $ops) and !$expecting_op) {
00272                 return $this->trigger("unexpected operator '$op'");
00273             } else { // I don't even want to know what you did to get here
00274                 return $this->trigger("an unexpected error occured");
00275             }
00276             if ($index == strlen($expr)) {
00277                 if (in_array($op, $ops)) { // did we end with an operator? bad.
00278                     return $this->trigger("operator '$op' lacks operand");
00279                 } else {
00280                     break;
00281                 }
00282             }
00283             while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace 
00284                 $index++;                             // into implicit multiplication if no operator is there)
00285             }
00286         
00287         } 
00288         while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
00289             if ($op == '(') return $this->trigger("expecting ')'"); // if there are (s on the stack, ()s were unbalanced
00290             $output[] = $op;
00291         }
00292         return $output;
00293     }
00294 
00295     // evaluate postfix notation
00296     function pfx($tokens, $vars = array()) {
00297         
00298         if ($tokens == false) return false;
00299     
00300         $stack = new EvalMathStack;
00301         
00302         foreach ($tokens as $token) { // nice and easy
00303             // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
00304             if (in_array($token, array('+', '-', '*', '/', '^'))) {
00305                 if (is_null($op2 = $stack->pop())) return $this->trigger("internal error");
00306                 if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
00307                 switch ($token) {
00308                     case '+':
00309                         $stack->push($op1+$op2); break;
00310                     case '-':
00311                         $stack->push($op1-$op2); break;
00312                     case '*':
00313                         $stack->push($op1*$op2); break;
00314                     case '/':
00315                         if ($op2 == 0) return $this->trigger("division by zero");
00316                         $stack->push($op1/$op2); break;
00317                     case '^':
00318                         $stack->push(pow($op1, $op2)); break;
00319                 }
00320             // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
00321             } elseif ($token == "_") {
00322                 $stack->push(-1*$stack->pop());
00323             // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
00324             } elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function!
00325                 $fnn = $matches[1];
00326                 if (in_array($fnn, $this->fb)) { // built-in function:
00327                     if (is_null($op1 = $stack->pop())) return $this->trigger("internal error");
00328                     $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
00329                     if ($fnn == 'ln') $fnn = 'log';
00330                     eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
00331                 } elseif (array_key_exists($fnn, $this->f)) { // user function
00332                     // get args
00333                     $args = array();
00334                     for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
00335                         if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger("internal error");
00336                     }
00337                     $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
00338                 }
00339             // if the token is a number or variable, push it on the stack
00340             } else {
00341                 if (is_numeric($token)) {
00342                     $stack->push($token);
00343                 } elseif (($hex=$this->from_hexbin($token))!==FALSE) {
00344                     $stack->push($hex);
00345                 } elseif (array_key_exists($token, $this->v)) {
00346                     $stack->push($this->v[$token]);
00347                 } elseif (array_key_exists($token, $vars)) {
00348                     $stack->push($vars[$token]);
00349                 } else {
00350                     return $this->trigger("undefined variable '$token'");
00351                 }
00352             }
00353         }
00354         // when we're out of tokens, the stack should have a single element, the final result
00355         if ($stack->count != 1) return $this->trigger("internal error");
00356         return $stack->pop();
00357     }
00358     
00359     // trigger an error, but nicely, if need be
00360     function trigger($msg) {
00361         $this->last_error = $msg;
00362         if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
00363         return false;
00364     }
00365     
00366     // check if the token is a hex/bin number, and convert to decimal
00367     //  1234h/0101010b are allowed
00368     function from_hexbin($token) {
00369       if (strtoupper(substr($token, -1, 1))=='H')  return hexdec($token);
00370       if (strtoupper(substr($token, -1, 1))=='B')  return bindec($token);
00371       return FALSE;
00372     }
00373 }
00374 
00375 // for internal use
00376 class EvalMathStack {
00377 
00378     var $stack = array();
00379     var $count = 0;
00380     
00381     function push($val) {
00382         $this->stack[$this->count] = $val;
00383         $this->count++;
00384     }
00385     
00386     function pop() {
00387         if ($this->count > 0) {
00388             $this->count--;
00389             return $this->stack[$this->count];
00390         }
00391         return null;
00392     }
00393     
00394     function last($n=1) {
00395         return $this->stack[$this->count-$n];
00396     }
00397 }
00398 
00399 ?>

Generated on Fri Dec 13 2013 17:56:57 for ILIAS Release_3_9_x_branch .rev 46835 by  doxygen 1.7.1