4 ================================================================================
6 EvalMath - PHP Class to safely evaluate math expressions
7 Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
9 ================================================================================
12 EvalMath - safely evaluate math expressions
16 include('evalmath.class.php');
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))');
25 $m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
27 $result = $m->evaluate('3*f(42,a)');
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!
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.
42 A synonym for $m->evaluate().
45 Returns an associative array of all user-defined variables and values.
48 Returns an array of all user-defined functions.
52 Set to true to turn off warnings when evaluating expressions
55 If the last evaluation failed, contains a string describing the error.
56 (Useful when suppress_errors is on).
59 Copyright 2005, Miles Kaufmann.
62 Redistribution and use in source and binary forms, with or without
63 modification, are permitted provided that the following conditions are
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
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.
90 * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
92 * This class was modified to allow comparison operators (<, <=, ==, >=, >)
93 * and synonyms functions (for the 'if' function). See MDL-14274 for more details.
98 /** @var string Pattern used for a valid function or variable name. Note, var and func names are case insensitive.*/
99 private static $namepat = '[a-z][a-z0-9_]*';
101 var $suppress_errors = false;
102 var $last_error = null;
104 var $v = array(); // variables (and constants)
105 var $f = array(); // user-defined functions
106 var $vb = array(); // constants
107 var $fb = array( // built-in functions
108 'sin','sinh','arcsin','asin','arcsinh','asinh',
109 'cos','cosh','arccos','acos','arccosh','acosh',
110 'tan','tanh','arctan','atan','arctanh','atanh',
111 'sqrt','abs','ln','log','exp','floor','ceil');
113 var $fc = array( // calc functions emulation
114 'average'=>array(-1), 'max'=>array(-1), 'min'=>array(-1),
115 'mod'=>array(2), 'pi'=>array(0), 'power'=>array(2),
116 'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
117 'rand_float'=>array(0), 'ifthenelse'=>array(3));
118 var $fcsynonyms = array('if' => 'ifthenelse');
120 var $allowimplicitmultiplication;
122 public function __construct($allowconstants = false, $allowimplicitmultiplication = false) {
123 if ($allowconstants){
124 $this->v
['pi'] = pi();
125 $this->v
['e'] = exp(1);
127 $this->allowimplicitmultiplication
= $allowimplicitmultiplication;
131 * Old syntax of class constructor. Deprecated in PHP7.
133 * @deprecated since Moodle 3.1
135 public function EvalMath($allowconstants = false, $allowimplicitmultiplication = false) {
136 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER
);
137 self
::__construct($allowconstants, $allowimplicitmultiplication);
141 return $this->evaluate($expr);
144 function evaluate($expr) {
145 $this->last_error
= null;
147 if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
149 // is it a variable assignment?
150 if (preg_match('/^\s*('.self
::$namepat.')\s*=\s*(.+)$/', $expr, $matches)) {
151 if (in_array($matches[1], $this->vb
)) { // make sure we're not assigning to a constant
152 return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1]));
154 if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
155 $this->v
[$matches[1]] = $tmp; // if so, stick it in the variable array
156 return $this->v
[$matches[1]]; // and return the resulting value
158 // is it a function assignment?
159 } elseif (preg_match('/^\s*('.self
::$namepat.')\s*\(\s*('.self
::$namepat.'(?:\s*,\s*'.self
::$namepat.')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
160 $fnn = $matches[1]; // get the function name
161 if (in_array($matches[1], $this->fb
)) { // make sure it isn't built in
162 return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1]));
164 $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
165 if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
166 for ($i = 0; $i<count($stack); $i++
) { // freeze the state of the non-argument variables
168 if (preg_match('/^'.self
::$namepat.'$/', $token) and !in_array($token, $args)) {
169 if (array_key_exists($token, $this->v
)) {
170 $stack[$i] = $this->v
[$token];
172 return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token));
176 $this->f
[$fnn] = array('args'=>$args, 'func'=>$stack);
180 return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
190 foreach ($this->f
as $fnn=>$dat)
191 $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
196 * @param string $name
197 * @return boolean Is this a valid var or function name?
199 public static function is_valid_var_or_func_name($name){
200 return preg_match('/'.self
::$namepat.'$/iA', $name);
203 //===================== HERE BE INTERNAL METHODS ====================\\
205 // Convert infix to postfix notation
206 function nfx($expr) {
209 $stack = new EvalMathStack
;
210 $output = array(); // postfix form of expression, to be passed to pfx()
211 $expr = trim(strtolower($expr));
212 // MDL-14274: new operators for comparison added.
213 $ops = array('+', '-', '*', '/', '^', '_', '>', '<', '<=', '>=', '==');
214 $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
215 $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>3, '<'=>3, '<='=>3, '>='=>3, '=='=>3); // operator precedence
217 $expecting_op = false; // we use this in syntax-checking the expression
218 // and determining when a - is a negation
220 if (preg_match("/[^\w\s+*^\/()\.,-<>=]/", $expr, $matches)) { // make sure the characters are all good
221 return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
224 while(1) { // 1 Infinite Loop ;)
225 // MDL-14274 Test two character operators.
226 $op = substr($expr, $index, 2);
227 if (!in_array($op, $ops)) {
228 // MDL-14274 Get one character operator.
229 $op = substr($expr, $index, 1); // get the first character at the current index
231 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
232 $ex = preg_match('/^('.self
::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
234 if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
235 $stack->push('_'); // put a negation on the stack
237 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
238 return $this->trigger(get_string('illegalcharacterunderscore', 'mathslib')); // but not in the input expression
240 } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
241 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
242 if (!$this->allowimplicitmultiplication
){
243 return $this->trigger(get_string('implicitmultiplicationnotallowed', 'mathslib'));
244 } else {// it's an implicit multiplication
249 // heart of the algorithm:
250 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])) {
251 $output[] = $stack->pop(); // pop stuff off the stack into the output
253 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
254 $stack->push($op); // finally put OUR operator onto the stack
255 $index +
= strlen($op);
256 $expecting_op = false;
258 } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
259 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
260 if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
261 else $output[] = $o2;
263 if (preg_match('/^('.self
::$namepat.')\($/', $stack->last(2), $matches)) { // did we just close a function?
264 $fnn = $matches[1]; // get the function name
265 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
267 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>$arg_count); // send function to output
268 if (in_array($fnn, $this->fb
)) { // check the argument count
272 $a->given
= $arg_count;
273 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
275 } elseif ($this->get_native_function_name($fnn)) {
276 $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
278 $counts = $this->fc
[$fnn];
279 if (in_array(-1, $counts) and $arg_count > 0) {}
280 elseif (!in_array($arg_count, $counts)) {
282 $a->expected
= implode('/',$this->fc
[$fnn]);
283 $a->given
= $arg_count;
284 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
286 } elseif (array_key_exists($fnn, $this->f
)) {
287 if ($arg_count != count($this->f
[$fnn]['args'])) {
289 $a->expected
= count($this->f
[$fnn]['args']);
290 $a->given
= $arg_count;
291 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
293 } else { // did we somehow push a non-function on the stack? this should never happen
294 return $this->trigger(get_string('internalerror', 'mathslib'));
299 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
300 while (($o2 = $stack->pop()) != '(') {
301 if (is_null($o2)) return $this->trigger(get_string('unexpectedcomma', 'mathslib')); // oops, never had a (
302 else $output[] = $o2; // pop the argument expression stuff and push onto the output
304 // make sure there was a function
305 if (!preg_match('/^('.self
::$namepat.')\($/', $stack->last(2), $matches))
306 return $this->trigger(get_string('unexpectedcomma', 'mathslib'));
307 $stack->push($stack->pop()+
1); // increment the argument count
308 $stack->push('('); // put the ( back on, we'll need to pop back to it again
310 $expecting_op = false;
312 } elseif ($op == '(' and !$expecting_op) {
313 $stack->push('('); // that was easy
317 } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
318 $expecting_op = true;
320 if (preg_match('/^('.self
::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
321 if (in_array($matches[1], $this->fb
) or
322 array_key_exists($matches[1], $this->f
) or
323 $this->get_native_function_name($matches[1])){ // it's a func
327 $expecting_op = false;
328 } else { // it's a var w/ implicit multiplication
332 } else { // it's a plain old var or num
335 $index +
= strlen($val);
337 } elseif ($op == ')') {
338 //it could be only custom function with no params or general error
339 if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
340 if (preg_match('/^('.self
::$namepat.')\($/', $stack->last(3), $matches)) { // did we just close a function?
344 $fnn = $matches[1]; // get the function name
345 $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
346 $counts = $this->fc
[$fnn];
347 if (!in_array(0, $counts)){
349 $a->expected
= $this->fc
[$fnn];
351 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
353 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>0); // send function to output
355 $expecting_op = true;
357 return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
360 } elseif (in_array($op, $ops) and !$expecting_op) { // miscellaneous error checking
361 return $this->trigger(get_string('unexpectedoperator', 'mathslib', $op));
362 } else { // I don't even want to know what you did to get here
363 return $this->trigger(get_string('anunexpectederroroccured', 'mathslib'));
365 if ($index == strlen($expr)) {
366 if (in_array($op, $ops)) { // did we end with an operator? bad.
367 return $this->trigger(get_string('operatorlacksoperand', 'mathslib', $op));
372 while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
373 $index++
; // into implicit multiplication if no operator is there)
377 while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
378 if ($op == '(') return $this->trigger(get_string('expectingaclosingbracket', 'mathslib')); // if there are (s on the stack, ()s were unbalanced
386 * @return string|boolean false if function name unknown.
388 function get_native_function_name($fnn) {
389 if (array_key_exists($fnn, $this->fcsynonyms
)) {
390 return $this->fcsynonyms
[$fnn];
391 } else if (array_key_exists($fnn, $this->fc
)) {
397 // evaluate postfix notation
398 function pfx($tokens, $vars = array()) {
400 if ($tokens == false) return false;
402 $stack = new EvalMathStack
;
404 foreach ($tokens as $token) { // nice and easy
406 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
407 if (is_array($token)) { // it's a function!
408 $fnn = $token['fnn'];
409 $count = $token['argcount'];
410 if (in_array($fnn, $this->fb
)) { // built-in function:
411 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
412 $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
413 if ($fnn == 'ln') $fnn = 'log';
414 eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
415 } elseif ($this->get_native_function_name($fnn)) { // calc emulation function
416 $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
419 for ($i = $count-1; $i >= 0; $i--) {
420 if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
422 $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args));
423 if ($res === FALSE) {
424 return $this->trigger(get_string('internalerror', 'mathslib'));
427 } elseif (array_key_exists($fnn, $this->f
)) { // user function
430 for ($i = count($this->f
[$fnn]['args'])-1; $i >= 0; $i--) {
431 if (is_null($args[$this->f
[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
433 $stack->push($this->pfx($this->f
[$fnn]['func'], $args)); // yay... recursion!!!!
435 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
436 } elseif (in_array($token, array('+', '-', '*', '/', '^', '>', '<', '==', '<=', '>='), true)) {
437 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
438 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
441 $stack->push($op1+
$op2); break;
443 $stack->push($op1-$op2); break;
445 $stack->push($op1*$op2); break;
447 if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib'));
448 $stack->push($op1/$op2); break;
450 $stack->push(pow($op1, $op2)); break;
452 $stack->push((int)($op1 > $op2)); break;
454 $stack->push((int)($op1 < $op2)); break;
456 $stack->push((int)($op1 == $op2)); break;
458 $stack->push((int)($op1 <= $op2)); break;
460 $stack->push((int)($op1 >= $op2)); break;
462 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
463 } elseif ($token == "_") {
464 $stack->push(-1*$stack->pop());
465 // if the token is a number or variable, push it on the stack
467 if (is_numeric($token)) {
468 $stack->push($token);
469 } elseif (array_key_exists($token, $this->v
)) {
470 $stack->push($this->v
[$token]);
471 } elseif (array_key_exists($token, $vars)) {
472 $stack->push($vars[$token]);
474 return $this->trigger(get_string('undefinedvariable', 'mathslib', $token));
478 // when we're out of tokens, the stack should have a single element, the final result
479 if ($stack->count
!= 1) return $this->trigger(get_string('internalerror', 'mathslib'));
480 return $stack->pop();
483 // trigger an error, but nicely, if need be
484 function trigger($msg) {
485 $this->last_error
= $msg;
486 if (!$this->suppress_errors
) trigger_error($msg, E_USER_WARNING
);
493 class EvalMathStack
{
495 var $stack = array();
498 function push($val) {
499 $this->stack
[$this->count
] = $val;
504 if ($this->count
> 0) {
506 return $this->stack
[$this->count
];
511 function last($n=1) {
512 if ($this->count
- $n >= 0) {
513 return $this->stack
[$this->count
-$n];
520 // spreadsheet functions emulation
521 class EvalMathFuncs
{
523 * MDL-14274 new conditional function.
524 * @param boolean $condition boolean for conditional.
525 * @param variant $then value if condition is true.
526 * @param unknown $else value if condition is false.
527 * @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
530 static function ifthenelse($condition, $then, $else) {
531 if ($condition == true) {
537 static function average() {
538 $args = func_get_args();
539 return (call_user_func_array(array('self', 'sum'), $args) / count($args));
542 static function max() {
543 $args = func_get_args();
544 $res = array_pop($args);
545 foreach($args as $a) {
553 static function min() {
554 $args = func_get_args();
555 $res = array_pop($args);
556 foreach($args as $a) {
564 static function mod($op1, $op2) {
568 static function pi() {
572 static function power($op1, $op2) {
573 return pow($op1, $op2);
576 static function round($val, $precision = 0) {
577 return round($val, $precision);
580 static function sum() {
581 $args = func_get_args();
583 foreach($args as $a) {
589 protected static $randomseed = null;
591 static function set_random_seed($randomseed) {
592 self
::$randomseed = $randomseed;
595 static function get_random_seed() {
596 if (is_null(self
::$randomseed)){
599 return self
::$randomseed;
603 static function rand_int($min, $max){
605 return false; //error
607 $noofchars = ceil(log($max +
1 - $min, '16'));
608 $md5string = md5(self
::get_random_seed());
611 while (($stringoffset +
$noofchars) > strlen($md5string)){
612 $md5string .= md5($md5string);
614 $randomno = hexdec(substr($md5string, $stringoffset, $noofchars));
615 $stringoffset +
= $noofchars;
616 } while (($min +
$randomno) > $max);
617 return $min +
$randomno;
620 static function rand_float() {
621 $randomvalues = unpack('v', md5(self
::get_random_seed(), true));
622 return array_shift($randomvalues) / 65536;