[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 3 /* 4 ================================================================================ 5 6 EvalMath - PHP Class to safely evaluate math expressions 7 Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/> 8 9 ================================================================================ 10 11 NAME 12 EvalMath - safely evaluate math expressions 13 14 SYNOPSIS 15 <? 16 include('evalmath.class.php'); 17 $m = new EvalMath; 18 // basic evaluation: 19 $result = $m->evaluate('2+2'); 20 // supports: order of operation; parentheses; negation; built-in functions 21 $result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8'); 22 // create your own variables 23 $m->evaluate('a = e^(ln(pi))'); 24 // or functions 25 $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1'); 26 // and then use them 27 $result = $m->evaluate('3*f(42,a)'); 28 ?> 29 30 DESCRIPTION 31 Use the EvalMath class when you want to evaluate mathematical expressions 32 from untrusted sources. You can define your own variables and functions, 33 which are stored in the object. Try it, it's fun! 34 35 METHODS 36 $m->evalute($expr) 37 Evaluates the expression and returns the result. If an error occurs, 38 prints a warning and returns false. If $expr is a function assignment, 39 returns true on success. 40 41 $m->e($expr) 42 A synonym for $m->evaluate(). 43 44 $m->vars() 45 Returns an associative array of all user-defined variables and values. 46 47 $m->funcs() 48 Returns an array of all user-defined functions. 49 50 PARAMETERS 51 $m->suppress_errors 52 Set to true to turn off warnings when evaluating expressions 53 54 $m->last_error 55 If the last evaluation failed, contains a string describing the error. 56 (Useful when suppress_errors is on). 57 58 AUTHOR INFORMATION 59 Copyright 2005, Miles Kaufmann. 60 61 LICENSE 62 Redistribution and use in source and binary forms, with or without 63 modification, are permitted provided that the following conditions are 64 met: 65 66 1 Redistributions of source code must retain the above copyright 67 notice, this list of conditions and the following disclaimer. 68 2. Redistributions in binary form must reproduce the above copyright 69 notice, this list of conditions and the following disclaimer in the 70 documentation and/or other materials provided with the distribution. 71 3. The name of the author may not be used to endorse or promote 72 products derived from this software without specific prior written 73 permission. 74 75 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 76 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 77 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 78 DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, 79 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 80 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 81 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 82 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 83 STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 84 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 85 POSSIBILITY OF SUCH DAMAGE. 86 87 */ 88 89 /** 90 * This class was heavily modified in order to get usefull spreadsheet emulation ;-) 91 * skodak 92 * 93 */ 94 95 class EvalMath { 96 97 /** @var string Pattern used for a valid function or variable name. Note, var and func names are case insensitive.*/ 98 private static $namepat = '[a-z][a-z0-9_]*'; 99 100 var $suppress_errors = false; 101 var $last_error = null; 102 103 var $v = array(); // variables (and constants) 104 var $f = array(); // user-defined functions 105 var $vb = array(); // constants 106 var $fb = array( // built-in functions 107 'sin','sinh','arcsin','asin','arcsinh','asinh', 108 'cos','cosh','arccos','acos','arccosh','acosh', 109 'tan','tanh','arctan','atan','arctanh','atanh', 110 'sqrt','abs','ln','log','exp','floor','ceil'); 111 112 var $fc = array( // calc functions emulation 113 'average'=>array(-1), 'max'=>array(-1), 'min'=>array(-1), 114 'mod'=>array(2), 'pi'=>array(0), 'power'=>array(2), 115 'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2), 116 'rand_float'=>array(0)); 117 118 var $allowimplicitmultiplication; 119 120 public function __construct($allowconstants = false, $allowimplicitmultiplication = false) { 121 if ($allowconstants){ 122 $this->v['pi'] = pi(); 123 $this->v['e'] = exp(1); 124 } 125 $this->allowimplicitmultiplication = $allowimplicitmultiplication; 126 } 127 128 /** 129 * Old syntax of class constructor. Deprecated in PHP7. 130 * 131 * @deprecated since Moodle 3.1 132 */ 133 public function EvalMath($allowconstants = false, $allowimplicitmultiplication = false) { 134 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 135 self::__construct($allowconstants, $allowimplicitmultiplication); 136 } 137 138 function e($expr) { 139 return $this->evaluate($expr); 140 } 141 142 function evaluate($expr) { 143 $this->last_error = null; 144 $expr = trim($expr); 145 if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end 146 //=============== 147 // is it a variable assignment? 148 if (preg_match('/^\s*('.self::$namepat.')\s*=\s*(.+)$/', $expr, $matches)) { 149 if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant 150 return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1])); 151 } 152 if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good 153 $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array 154 return $this->v[$matches[1]]; // and return the resulting value 155 //=============== 156 // is it a function assignment? 157 } elseif (preg_match('/^\s*('.self::$namepat.')\s*\(\s*('.self::$namepat.'(?:\s*,\s*'.self::$namepat.')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { 158 $fnn = $matches[1]; // get the function name 159 if (in_array($matches[1], $this->fb)) { // make sure it isn't built in 160 return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1])); 161 } 162 $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments 163 if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix 164 for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables 165 $token = $stack[$i]; 166 if (preg_match('/^'.self::$namepat.'$/', $token) and !in_array($token, $args)) { 167 if (array_key_exists($token, $this->v)) { 168 $stack[$i] = $this->v[$token]; 169 } else { 170 return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token)); 171 } 172 } 173 } 174 $this->f[$fnn] = array('args'=>$args, 'func'=>$stack); 175 return true; 176 //=============== 177 } else { 178 return $this->pfx($this->nfx($expr)); // straight up evaluation, woo 179 } 180 } 181 182 function vars() { 183 return $this->v; 184 } 185 186 function funcs() { 187 $output = array(); 188 foreach ($this->f as $fnn=>$dat) 189 $output[] = $fnn . '(' . implode(',', $dat['args']) . ')'; 190 return $output; 191 } 192 193 /** 194 * @param string $name 195 * @return boolean Is this a valid var or function name? 196 */ 197 public static function is_valid_var_or_func_name($name){ 198 return preg_match('/'.self::$namepat.'$/iA', $name); 199 } 200 201 //===================== HERE BE INTERNAL METHODS ====================\\ 202 203 // Convert infix to postfix notation 204 function nfx($expr) { 205 206 $index = 0; 207 $stack = new EvalMathStack; 208 $output = array(); // postfix form of expression, to be passed to pfx() 209 $expr = trim(strtolower($expr)); 210 211 $ops = array('+', '-', '*', '/', '^', '_'); 212 $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator? 213 $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence 214 215 $expecting_op = false; // we use this in syntax-checking the expression 216 // and determining when a - is a negation 217 218 if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good 219 return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0])); 220 } 221 222 while(1) { // 1 Infinite Loop ;) 223 $op = substr($expr, $index, 1); // get the first character at the current index 224 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand 225 $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match); 226 //=============== 227 if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus? 228 $stack->push('_'); // put a negation on the stack 229 $index++; 230 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack 231 return $this->trigger(get_string('illegalcharacterunderscore', 'mathslib')); // but not in the input expression 232 //=============== 233 } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack? 234 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis? 235 if (!$this->allowimplicitmultiplication){ 236 return $this->trigger(get_string('implicitmultiplicationnotallowed', 'mathslib')); 237 } else {// it's an implicit multiplication 238 $op = '*'; 239 $index--; 240 } 241 } 242 // heart of the algorithm: 243 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])) { 244 $output[] = $stack->pop(); // pop stuff off the stack into the output 245 } 246 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail 247 $stack->push($op); // finally put OUR operator onto the stack 248 $index++; 249 $expecting_op = false; 250 //=============== 251 } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis? 252 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last ( 253 if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); 254 else $output[] = $o2; 255 } 256 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches)) { // did we just close a function? 257 $fnn = $matches[1]; // get the function name 258 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) 259 $fn = $stack->pop(); 260 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>$arg_count); // send function to output 261 if (in_array($fnn, $this->fb)) { // check the argument count 262 if($arg_count > 1) { 263 $a= new stdClass(); 264 $a->expected = 1; 265 $a->given = $arg_count; 266 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); 267 } 268 } elseif (array_key_exists($fnn, $this->fc)) { 269 $counts = $this->fc[$fnn]; 270 if (in_array(-1, $counts) and $arg_count > 0) {} 271 elseif (!in_array($arg_count, $counts)) { 272 $a= new stdClass(); 273 $a->expected = implode('/',$this->fc[$fnn]); 274 $a->given = $arg_count; 275 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); 276 } 277 } elseif (array_key_exists($fnn, $this->f)) { 278 if ($arg_count != count($this->f[$fnn]['args'])) { 279 $a= new stdClass(); 280 $a->expected = count($this->f[$fnn]['args']); 281 $a->given = $arg_count; 282 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); 283 } 284 } else { // did we somehow push a non-function on the stack? this should never happen 285 return $this->trigger(get_string('internalerror', 'mathslib')); 286 } 287 } 288 $index++; 289 //=============== 290 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument? 291 while (($o2 = $stack->pop()) != '(') { 292 if (is_null($o2)) return $this->trigger(get_string('unexpectedcomma', 'mathslib')); // oops, never had a ( 293 else $output[] = $o2; // pop the argument expression stuff and push onto the output 294 } 295 // make sure there was a function 296 if (!preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches)) 297 return $this->trigger(get_string('unexpectedcomma', 'mathslib')); 298 $stack->push($stack->pop()+1); // increment the argument count 299 $stack->push('('); // put the ( back on, we'll need to pop back to it again 300 $index++; 301 $expecting_op = false; 302 //=============== 303 } elseif ($op == '(' and !$expecting_op) { 304 $stack->push('('); // that was easy 305 $index++; 306 $allow_neg = true; 307 //=============== 308 } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number? 309 $expecting_op = true; 310 $val = $match[1]; 311 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... 312 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 313 $stack->push($val); 314 $stack->push(1); 315 $stack->push('('); 316 $expecting_op = false; 317 } else { // it's a var w/ implicit multiplication 318 $val = $matches[1]; 319 $output[] = $val; 320 } 321 } else { // it's a plain old var or num 322 $output[] = $val; 323 } 324 $index += strlen($val); 325 //=============== 326 } elseif ($op == ')') { 327 //it could be only custom function with no params or general error 328 if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); 329 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(3), $matches)) { // did we just close a function? 330 $stack->pop();// ( 331 $stack->pop();// 1 332 $fn = $stack->pop(); 333 $fnn = $matches[1]; // get the function name 334 $counts = $this->fc[$fnn]; 335 if (!in_array(0, $counts)){ 336 $a= new stdClass(); 337 $a->expected = $this->fc[$fnn]; 338 $a->given = 0; 339 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a)); 340 } 341 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>0); // send function to output 342 $index++; 343 $expecting_op = true; 344 } else { 345 return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib')); 346 } 347 //=============== 348 } elseif (in_array($op, $ops) and !$expecting_op) { // miscellaneous error checking 349 return $this->trigger(get_string('unexpectedoperator', 'mathslib', $op)); 350 } else { // I don't even want to know what you did to get here 351 return $this->trigger(get_string('anunexpectederroroccured', 'mathslib')); 352 } 353 if ($index == strlen($expr)) { 354 if (in_array($op, $ops)) { // did we end with an operator? bad. 355 return $this->trigger(get_string('operatorlacksoperand', 'mathslib', $op)); 356 } else { 357 break; 358 } 359 } 360 while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace 361 $index++; // into implicit multiplication if no operator is there) 362 } 363 364 } 365 while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output 366 if ($op == '(') return $this->trigger(get_string('expectingaclosingbracket', 'mathslib')); // if there are (s on the stack, ()s were unbalanced 367 $output[] = $op; 368 } 369 return $output; 370 } 371 372 // evaluate postfix notation 373 function pfx($tokens, $vars = array()) { 374 375 if ($tokens == false) return false; 376 377 $stack = new EvalMathStack; 378 379 foreach ($tokens as $token) { // nice and easy 380 381 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on 382 if (is_array($token)) { // it's a function! 383 $fnn = $token['fnn']; 384 $count = $token['argcount']; 385 if (in_array($fnn, $this->fb)) { // built-in function: 386 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); 387 $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms 388 if ($fnn == 'ln') $fnn = 'log'; 389 eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval() 390 } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function 391 // get args 392 $args = array(); 393 for ($i = $count-1; $i >= 0; $i--) { 394 if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); 395 } 396 $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args)); 397 if ($res === FALSE) { 398 return $this->trigger(get_string('internalerror', 'mathslib')); 399 } 400 $stack->push($res); 401 } elseif (array_key_exists($fnn, $this->f)) { // user function 402 // get args 403 $args = array(); 404 for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) { 405 if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); 406 } 407 $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!! 408 } 409 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on 410 } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) { 411 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); 412 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib')); 413 switch ($token) { 414 case '+': 415 $stack->push($op1+$op2); break; 416 case '-': 417 $stack->push($op1-$op2); break; 418 case '*': 419 $stack->push($op1*$op2); break; 420 case '/': 421 if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib')); 422 $stack->push($op1/$op2); break; 423 case '^': 424 $stack->push(pow($op1, $op2)); break; 425 } 426 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on 427 } elseif ($token == "_") { 428 $stack->push(-1*$stack->pop()); 429 // if the token is a number or variable, push it on the stack 430 } else { 431 if (is_numeric($token)) { 432 $stack->push($token); 433 } elseif (array_key_exists($token, $this->v)) { 434 $stack->push($this->v[$token]); 435 } elseif (array_key_exists($token, $vars)) { 436 $stack->push($vars[$token]); 437 } else { 438 return $this->trigger(get_string('undefinedvariable', 'mathslib', $token)); 439 } 440 } 441 } 442 // when we're out of tokens, the stack should have a single element, the final result 443 if ($stack->count != 1) return $this->trigger(get_string('internalerror', 'mathslib')); 444 return $stack->pop(); 445 } 446 447 // trigger an error, but nicely, if need be 448 function trigger($msg) { 449 $this->last_error = $msg; 450 if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING); 451 return false; 452 } 453 454 } 455 456 // for internal use 457 class EvalMathStack { 458 459 var $stack = array(); 460 var $count = 0; 461 462 function push($val) { 463 $this->stack[$this->count] = $val; 464 $this->count++; 465 } 466 467 function pop() { 468 if ($this->count > 0) { 469 $this->count--; 470 return $this->stack[$this->count]; 471 } 472 return null; 473 } 474 475 function last($n=1) { 476 if ($this->count - $n >= 0) { 477 return $this->stack[$this->count-$n]; 478 } 479 return null; 480 } 481 } 482 483 484 // spreadsheet functions emulation 485 class EvalMathFuncs { 486 487 static function average() { 488 $args = func_get_args(); 489 return (call_user_func_array(array('self', 'sum'), $args) / count($args)); 490 } 491 492 static function max() { 493 $args = func_get_args(); 494 $res = array_pop($args); 495 foreach($args as $a) { 496 if ($res < $a) { 497 $res = $a; 498 } 499 } 500 return $res; 501 } 502 503 static function min() { 504 $args = func_get_args(); 505 $res = array_pop($args); 506 foreach($args as $a) { 507 if ($res > $a) { 508 $res = $a; 509 } 510 } 511 return $res; 512 } 513 514 static function mod($op1, $op2) { 515 return $op1 % $op2; 516 } 517 518 static function pi() { 519 return pi(); 520 } 521 522 static function power($op1, $op2) { 523 return pow($op1, $op2); 524 } 525 526 static function round($val, $precision = 0) { 527 return round($val, $precision); 528 } 529 530 static function sum() { 531 $args = func_get_args(); 532 $res = 0; 533 foreach($args as $a) { 534 $res += $a; 535 } 536 return $res; 537 } 538 539 protected static $randomseed = null; 540 541 static function set_random_seed($randomseed) { 542 self::$randomseed = $randomseed; 543 } 544 545 static function get_random_seed() { 546 if (is_null(self::$randomseed)){ 547 return microtime(); 548 } else { 549 return self::$randomseed; 550 } 551 } 552 553 static function rand_int($min, $max){ 554 if ($min >= $max) { 555 return false; //error 556 } 557 $noofchars = ceil(log($max + 1 - $min, '16')); 558 $md5string = md5(self::get_random_seed()); 559 $stringoffset = 0; 560 do { 561 while (($stringoffset + $noofchars) > strlen($md5string)){ 562 $md5string .= md5($md5string); 563 } 564 $randomno = hexdec(substr($md5string, $stringoffset, $noofchars)); 565 $stringoffset += $noofchars; 566 } while (($min + $randomno) > $max); 567 return $min + $randomno; 568 } 569 570 static function rand_float() { 571 $randomvalues = unpack('v', md5(self::get_random_seed(), true)); 572 return array_shift($randomvalues) / 65536; 573 } 574 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |