|
Moodle
2.2.1
http://www.collinsharper.com
|
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 }