Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/lib/evalmath/evalmath.class.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 
00095 class EvalMath {
00096 
00098     private static $namepat = '[a-z][a-z0-9_]*';
00099 
00100     var $suppress_errors = false;
00101     var $last_error = null;
00102 
00103     var $v = array(); // variables (and constants)
00104     var $f = array(); // user-defined functions
00105     var $vb = array(); // constants
00106     var $fb = array(  // built-in functions
00107         'sin','sinh','arcsin','asin','arcsinh','asinh',
00108         'cos','cosh','arccos','acos','arccosh','acosh',
00109         'tan','tanh','arctan','atan','arctanh','atanh',
00110         'sqrt','abs','ln','log','exp','floor','ceil','round');
00111 
00112     var $fc = array( // calc functions emulation
00113         'average'=>array(-1), 'max'=>array(-1),  'min'=>array(-1),
00114         'mod'=>array(2),      'pi'=>array(0),    'power'=>array(2),
00115         'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
00116         'rand_float'=>array(0));
00117 
00118     var $allowimplicitmultiplication;
00119 
00120     function EvalMath($allowconstants = false, $allowimplicitmultiplication = false) {
00121         if ($allowconstants){
00122             $this->v['pi'] = pi();
00123             $this->v['e'] = exp(1);
00124         }
00125         $this->allowimplicitmultiplication = $allowimplicitmultiplication;
00126     }
00127 
00128     function e($expr) {
00129         return $this->evaluate($expr);
00130     }
00131 
00132     function evaluate($expr) {
00133         $this->last_error = null;
00134         $expr = trim($expr);
00135         if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
00136         //===============
00137         // is it a variable assignment?
00138         if (preg_match('/^\s*('.self::$namepat.')\s*=\s*(.+)$/', $expr, $matches)) {
00139             if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
00140                 return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1]));
00141             }
00142             if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
00143             $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
00144             return $this->v[$matches[1]]; // and return the resulting value
00145         //===============
00146         // is it a function assignment?
00147         } elseif (preg_match('/^\s*('.self::$namepat.')\s*\(\s*('.self::$namepat.'(?:\s*,\s*'.self::$namepat.')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
00148             $fnn = $matches[1]; // get the function name
00149             if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
00150                 return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1]));
00151             }
00152             $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
00153             if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
00154             for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
00155                 $token = $stack[$i];
00156                 if (preg_match('/^'.self::$namepat.'$/', $token) and !in_array($token, $args)) {
00157                     if (array_key_exists($token, $this->v)) {
00158                         $stack[$i] = $this->v[$token];
00159                     } else {
00160                         return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token));
00161                     }
00162                 }
00163             }
00164             $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
00165             return true;
00166         //===============
00167         } else {
00168             return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
00169         }
00170     }
00171 
00172     function vars() {
00173         return $this->v;
00174     }
00175 
00176     function funcs() {
00177         $output = array();
00178         foreach ($this->f as $fnn=>$dat)
00179             $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
00180         return $output;
00181     }
00182 
00187     public static function is_valid_var_or_func_name($name){
00188         return preg_match('/'.self::$namepat.'$/iA', $name);
00189     }
00190 
00191     //===================== HERE BE INTERNAL METHODS ====================\\
00192 
00193     // Convert infix to postfix notation
00194     function nfx($expr) {
00195 
00196         $index = 0;
00197         $stack = new EvalMathStack;
00198         $output = array(); // postfix form of expression, to be passed to pfx()
00199         $expr = trim(strtolower($expr));
00200 
00201         $ops   = array('+', '-', '*', '/', '^', '_');
00202         $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
00203         $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
00204 
00205         $expecting_op = false; // we use this in syntax-checking the expression
00206                                // and determining when a - is a negation
00207 
00208         if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
00209             return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
00210         }
00211 
00212         while(1) { // 1 Infinite Loop ;)
00213             $op = substr($expr, $index, 1); // get the first character at the current index
00214             // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
00215             $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
00216             //===============
00217             if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
00218                 $stack->push('_'); // put a negation on the stack
00219                 $index++;
00220             } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
00221                 return $this->trigger(get_string('illegalcharacterunderscore', 'mathslib')); // but not in the input expression
00222             //===============
00223             } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
00224                 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
00225                     if (!$this->allowimplicitmultiplication){
00226                         return $this->trigger(get_string('implicitmultiplicationnotallowed', 'mathslib'));
00227                     } else {// it's an implicit multiplication
00228                         $op = '*';
00229                         $index--;
00230                     }
00231                 }
00232                 // heart of the algorithm:
00233                 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])) {
00234                     $output[] = $stack->pop(); // pop stuff off the stack into the output
00235                 }
00236                 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
00237                 $stack->push($op); // finally put OUR operator onto the stack
00238                 $index++;
00239                 $expecting_op = false;
00240             //===============
00241             } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
00242                 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
00243                     if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
00244                     else $output[] = $o2;
00245                 }
00246                 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches)) { // did we just close a function?
00247                     $fnn = $matches[1]; // get the function name
00248                     $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
00249                     $fn = $stack->pop();
00250                     $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>$arg_count); // send function to output
00251                     if (in_array($fnn, $this->fb)) { // check the argument count
00252                         if($arg_count > 1) {
00253                             $a= new stdClass();
00254                             $a->expected = 1;
00255                             $a->given = $arg_count;
00256                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
00257                         }
00258                     } elseif (array_key_exists($fnn, $this->fc)) {
00259                         $counts = $this->fc[$fnn];
00260                         if (in_array(-1, $counts) and $arg_count > 0) {}
00261                         elseif (!in_array($arg_count, $counts)) {
00262                             $a= new stdClass();
00263                             $a->expected = implode('/',$this->fc[$fnn]);
00264                             $a->given = $arg_count;
00265                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
00266                         }
00267                     } elseif (array_key_exists($fnn, $this->f)) {
00268                         if ($arg_count != count($this->f[$fnn]['args'])) {
00269                             $a= new stdClass();
00270                             $a->expected = count($this->f[$fnn]['args']);
00271                             $a->given = $arg_count;
00272                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
00273                         }
00274                     } else { // did we somehow push a non-function on the stack? this should never happen
00275                         return $this->trigger(get_string('internalerror', 'mathslib'));
00276                     }
00277                 }
00278                 $index++;
00279             //===============
00280             } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
00281                 while (($o2 = $stack->pop()) != '(') {
00282                     if (is_null($o2)) return $this->trigger(get_string('unexpectedcomma', 'mathslib')); // oops, never had a (
00283                     else $output[] = $o2; // pop the argument expression stuff and push onto the output
00284                 }
00285                 // make sure there was a function
00286                 if (!preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches))
00287                     return $this->trigger(get_string('unexpectedcomma', 'mathslib'));
00288                 $stack->push($stack->pop()+1); // increment the argument count
00289                 $stack->push('('); // put the ( back on, we'll need to pop back to it again
00290                 $index++;
00291                 $expecting_op = false;
00292             //===============
00293             } elseif ($op == '(' and !$expecting_op) {
00294                 $stack->push('('); // that was easy
00295                 $index++;
00296                 $allow_neg = true;
00297             //===============
00298             } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
00299                 $expecting_op = true;
00300                 $val = $match[1];
00301                 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
00302                     if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func
00303                         $stack->push($val);
00304                         $stack->push(1);
00305                         $stack->push('(');
00306                         $expecting_op = false;
00307                     } else { // it's a var w/ implicit multiplication
00308                         $val = $matches[1];
00309                         $output[] = $val;
00310                     }
00311                 } else { // it's a plain old var or num
00312                     $output[] = $val;
00313                 }
00314                 $index += strlen($val);
00315             //===============
00316             } elseif ($op == ')') {
00317                 //it could be only custom function with no params or general error
00318                 if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
00319                 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(3), $matches)) { // did we just close a function?
00320                     $stack->pop();// (
00321                     $stack->pop();// 1
00322                     $fn = $stack->pop();
00323                     $fnn = $matches[1]; // get the function name
00324                     $counts = $this->fc[$fnn];
00325                     if (!in_array(0, $counts)){
00326                         $a= new stdClass();
00327                         $a->expected = $this->fc[$fnn];
00328                         $a->given = 0;
00329                         return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
00330                     }
00331                     $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>0); // send function to output
00332                     $index++;
00333                     $expecting_op = true;
00334                 } else {
00335                     return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
00336                 }
00337             //===============
00338             } elseif (in_array($op, $ops) and !$expecting_op) { // miscellaneous error checking
00339                 return $this->trigger(get_string('unexpectedoperator', 'mathslib', $op));
00340             } else { // I don't even want to know what you did to get here
00341                 return $this->trigger(get_string('anunexpectederroroccured', 'mathslib'));
00342             }
00343             if ($index == strlen($expr)) {
00344                 if (in_array($op, $ops)) { // did we end with an operator? bad.
00345                     return $this->trigger(get_string('operatorlacksoperand', 'mathslib', $op));
00346                 } else {
00347                     break;
00348                 }
00349             }
00350             while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
00351                 $index++;                             // into implicit multiplication if no operator is there)
00352             }
00353 
00354         }
00355         while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
00356             if ($op == '(') return $this->trigger(get_string('expectingaclosingbracket', 'mathslib')); // if there are (s on the stack, ()s were unbalanced
00357             $output[] = $op;
00358         }
00359         return $output;
00360     }
00361 
00362     // evaluate postfix notation
00363     function pfx($tokens, $vars = array()) {
00364 
00365         if ($tokens == false) return false;
00366 
00367         $stack = new EvalMathStack;
00368 
00369         foreach ($tokens as $token) { // nice and easy
00370 
00371             // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
00372             if (is_array($token)) { // it's a function!
00373                 $fnn = $token['fnn'];
00374                 $count = $token['argcount'];
00375                 if (in_array($fnn, $this->fb)) { // built-in function:
00376                     if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
00377                     $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
00378                     if ($fnn == 'ln') $fnn = 'log';
00379                     eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
00380                 } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function
00381                     // get args
00382                     $args = array();
00383                     for ($i = $count-1; $i >= 0; $i--) {
00384                         if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
00385                     }
00386                     $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args));
00387                     if ($res === FALSE) {
00388                         return $this->trigger(get_string('internalerror', 'mathslib'));
00389                     }
00390                     $stack->push($res);
00391                 } elseif (array_key_exists($fnn, $this->f)) { // user function
00392                     // get args
00393                     $args = array();
00394                     for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
00395                         if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
00396                     }
00397                     $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
00398                 }
00399             // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
00400             } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) {
00401                 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
00402                 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
00403                 switch ($token) {
00404                     case '+':
00405                         $stack->push($op1+$op2); break;
00406                     case '-':
00407                         $stack->push($op1-$op2); break;
00408                     case '*':
00409                         $stack->push($op1*$op2); break;
00410                     case '/':
00411                         if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib'));
00412                         $stack->push($op1/$op2); break;
00413                     case '^':
00414                         $stack->push(pow($op1, $op2)); break;
00415                 }
00416             // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
00417             } elseif ($token == "_") {
00418                 $stack->push(-1*$stack->pop());
00419             // if the token is a number or variable, push it on the stack
00420             } else {
00421                 if (is_numeric($token)) {
00422                     $stack->push($token);
00423                 } elseif (array_key_exists($token, $this->v)) {
00424                     $stack->push($this->v[$token]);
00425                 } elseif (array_key_exists($token, $vars)) {
00426                     $stack->push($vars[$token]);
00427                 } else {
00428                     return $this->trigger(get_string('undefinedvariable', 'mathslib', $token));
00429                 }
00430             }
00431         }
00432         // when we're out of tokens, the stack should have a single element, the final result
00433         if ($stack->count != 1) return $this->trigger(get_string('internalerror', 'mathslib'));
00434         return $stack->pop();
00435     }
00436 
00437     // trigger an error, but nicely, if need be
00438     function trigger($msg) {
00439         $this->last_error = $msg;
00440         if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
00441         return false;
00442     }
00443 
00444 }
00445 
00446 // for internal use
00447 class EvalMathStack {
00448 
00449     var $stack = array();
00450     var $count = 0;
00451 
00452     function push($val) {
00453         $this->stack[$this->count] = $val;
00454         $this->count++;
00455     }
00456 
00457     function pop() {
00458         if ($this->count > 0) {
00459             $this->count--;
00460             return $this->stack[$this->count];
00461         }
00462         return null;
00463     }
00464 
00465     function last($n=1) {
00466         if ($this->count - $n >= 0) {
00467             return $this->stack[$this->count-$n];
00468         }
00469         return null;
00470     }
00471 }
00472 
00473 
00474 // spreadsheet functions emulation
00475 class EvalMathFuncs {
00476 
00477     static function average() {
00478         $args = func_get_args();
00479         return (call_user_func_array(array('self', 'sum'), $args) / count($args));
00480     }
00481 
00482     static function max() {
00483         $args = func_get_args();
00484         $res = array_pop($args);
00485         foreach($args as $a) {
00486             if ($res < $a) {
00487                 $res = $a;
00488             }
00489         }
00490         return $res;
00491     }
00492 
00493     static function min() {
00494         $args = func_get_args();
00495         $res = array_pop($args);
00496         foreach($args as $a) {
00497             if ($res > $a) {
00498                 $res = $a;
00499             }
00500         }
00501         return $res;
00502     }
00503 
00504     static function mod($op1, $op2) {
00505         return $op1 % $op2;
00506     }
00507 
00508     static function pi() {
00509         return pi();
00510     }
00511 
00512     static function power($op1, $op2) {
00513         return pow($op1, $op2);
00514     }
00515 
00516     static function round($val, $precision = 0) {
00517         return round($val, $precision);
00518     }
00519 
00520     static function sum() {
00521         $args = func_get_args();
00522         $res = 0;
00523         foreach($args as $a) {
00524            $res += $a;
00525         }
00526         return $res;
00527     }
00528 
00529     protected static $randomseed = null;
00530 
00531     static function set_random_seed($randomseed) {
00532         self::$randomseed = $randomseed;
00533     }
00534 
00535     static function get_random_seed() {
00536         if (is_null(self::$randomseed)){
00537             return microtime();
00538         } else {
00539             return self::$randomseed;
00540         }
00541     }
00542 
00543     static function rand_int($min, $max){
00544         if ($min >= $max) {
00545             return false; //error
00546         }
00547         $noofchars = ceil(log($max + 1 - $min, '16'));
00548         $md5string = md5(self::get_random_seed());
00549         $stringoffset = 0;
00550         do {
00551             while (($stringoffset + $noofchars) > strlen($md5string)){
00552                 $md5string .= md5($md5string);
00553             }
00554             $randomno = hexdec(substr($md5string, $stringoffset, $noofchars));
00555             $stringoffset += $noofchars;
00556         } while (($min + $randomno) > $max);
00557         return $min + $randomno;
00558     }
00559 
00560     static function rand_float(){
00561         $randomvalue = array_shift(unpack('v', md5(self::get_random_seed(), true)));
00562         return $randomvalue / 65536;
00563     }
00564 
00565 }
 All Data Structures Namespaces Files Functions Variables Enumerations