Merge branch 'MDL-54553-30' of git://github.com/danpoltawski/moodle into MOODLE_30_STABLE
[moodle.git] / lib / evalmath / evalmath.class.php
blob2848714f41d66452783b41149cfe41f86c97c13c
1 <?php
3 /*
4 ================================================================================
6 EvalMath - PHP Class to safely evaluate math expressions
7 Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
9 ================================================================================
11 NAME
12 EvalMath - safely evaluate math expressions
14 SYNOPSIS
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)');
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!
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.
41 $m->e($expr)
42 A synonym for $m->evaluate().
44 $m->vars()
45 Returns an associative array of all user-defined variables and values.
47 $m->funcs()
48 Returns an array of all user-defined functions.
50 PARAMETERS
51 $m->suppress_errors
52 Set to true to turn off warnings when evaluating expressions
54 $m->last_error
55 If the last evaluation failed, contains a string describing the error.
56 (Useful when suppress_errors is on).
58 AUTHOR INFORMATION
59 Copyright 2005, Miles Kaufmann.
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:
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.
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.
89 /**
90 * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
91 * skodak
95 class EvalMath {
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_]*';
100 var $suppress_errors = false;
101 var $last_error = null;
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');
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));
118 var $allowimplicitmultiplication;
120 public function __construct($allowconstants = false, $allowimplicitmultiplication = false) {
121 if ($allowconstants){
122 $this->v['pi'] = pi();
123 $this->v['e'] = exp(1);
125 $this->allowimplicitmultiplication = $allowimplicitmultiplication;
129 * Old syntax of class constructor. Deprecated in PHP7.
131 public function EvalMath($allowconstants = false, $allowimplicitmultiplication = false) {
132 self::__construct($allowconstants, $allowimplicitmultiplication);
135 function e($expr) {
136 return $this->evaluate($expr);
139 function evaluate($expr) {
140 $this->last_error = null;
141 $expr = trim($expr);
142 if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
143 //===============
144 // is it a variable assignment?
145 if (preg_match('/^\s*('.self::$namepat.')\s*=\s*(.+)$/', $expr, $matches)) {
146 if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
147 return $this->trigger(get_string('cannotassigntoconstant', 'mathslib', $matches[1]));
149 if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
150 $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
151 return $this->v[$matches[1]]; // and return the resulting value
152 //===============
153 // is it a function assignment?
154 } elseif (preg_match('/^\s*('.self::$namepat.')\s*\(\s*('.self::$namepat.'(?:\s*,\s*'.self::$namepat.')*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
155 $fnn = $matches[1]; // get the function name
156 if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
157 return $this->trigger(get_string('cannotredefinebuiltinfunction', 'mathslib', $matches[1]));
159 $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
160 if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
161 for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
162 $token = $stack[$i];
163 if (preg_match('/^'.self::$namepat.'$/', $token) and !in_array($token, $args)) {
164 if (array_key_exists($token, $this->v)) {
165 $stack[$i] = $this->v[$token];
166 } else {
167 return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token));
171 $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
172 return true;
173 //===============
174 } else {
175 return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
179 function vars() {
180 return $this->v;
183 function funcs() {
184 $output = array();
185 foreach ($this->f as $fnn=>$dat)
186 $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
187 return $output;
191 * @param string $name
192 * @return boolean Is this a valid var or function name?
194 public static function is_valid_var_or_func_name($name){
195 return preg_match('/'.self::$namepat.'$/iA', $name);
198 //===================== HERE BE INTERNAL METHODS ====================\\
200 // Convert infix to postfix notation
201 function nfx($expr) {
203 $index = 0;
204 $stack = new EvalMathStack;
205 $output = array(); // postfix form of expression, to be passed to pfx()
206 $expr = trim(strtolower($expr));
208 $ops = array('+', '-', '*', '/', '^', '_');
209 $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
210 $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
212 $expecting_op = false; // we use this in syntax-checking the expression
213 // and determining when a - is a negation
215 if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
216 return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
219 while(1) { // 1 Infinite Loop ;)
220 $op = substr($expr, $index, 1); // get the first character at the current index
221 // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
222 $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
223 //===============
224 if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
225 $stack->push('_'); // put a negation on the stack
226 $index++;
227 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
228 return $this->trigger(get_string('illegalcharacterunderscore', 'mathslib')); // but not in the input expression
229 //===============
230 } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
231 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
232 if (!$this->allowimplicitmultiplication){
233 return $this->trigger(get_string('implicitmultiplicationnotallowed', 'mathslib'));
234 } else {// it's an implicit multiplication
235 $op = '*';
236 $index--;
239 // heart of the algorithm:
240 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])) {
241 $output[] = $stack->pop(); // pop stuff off the stack into the output
243 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
244 $stack->push($op); // finally put OUR operator onto the stack
245 $index++;
246 $expecting_op = false;
247 //===============
248 } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
249 while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
250 if (is_null($o2)) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
251 else $output[] = $o2;
253 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches)) { // did we just close a function?
254 $fnn = $matches[1]; // get the function name
255 $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
256 $fn = $stack->pop();
257 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>$arg_count); // send function to output
258 if (in_array($fnn, $this->fb)) { // check the argument count
259 if($arg_count > 1) {
260 $a= new stdClass();
261 $a->expected = 1;
262 $a->given = $arg_count;
263 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
265 } elseif (array_key_exists($fnn, $this->fc)) {
266 $counts = $this->fc[$fnn];
267 if (in_array(-1, $counts) and $arg_count > 0) {}
268 elseif (!in_array($arg_count, $counts)) {
269 $a= new stdClass();
270 $a->expected = implode('/',$this->fc[$fnn]);
271 $a->given = $arg_count;
272 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
274 } elseif (array_key_exists($fnn, $this->f)) {
275 if ($arg_count != count($this->f[$fnn]['args'])) {
276 $a= new stdClass();
277 $a->expected = count($this->f[$fnn]['args']);
278 $a->given = $arg_count;
279 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
281 } else { // did we somehow push a non-function on the stack? this should never happen
282 return $this->trigger(get_string('internalerror', 'mathslib'));
285 $index++;
286 //===============
287 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
288 while (($o2 = $stack->pop()) != '(') {
289 if (is_null($o2)) return $this->trigger(get_string('unexpectedcomma', 'mathslib')); // oops, never had a (
290 else $output[] = $o2; // pop the argument expression stuff and push onto the output
292 // make sure there was a function
293 if (!preg_match('/^('.self::$namepat.')\($/', $stack->last(2), $matches))
294 return $this->trigger(get_string('unexpectedcomma', 'mathslib'));
295 $stack->push($stack->pop()+1); // increment the argument count
296 $stack->push('('); // put the ( back on, we'll need to pop back to it again
297 $index++;
298 $expecting_op = false;
299 //===============
300 } elseif ($op == '(' and !$expecting_op) {
301 $stack->push('('); // that was easy
302 $index++;
303 $allow_neg = true;
304 //===============
305 } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
306 $expecting_op = true;
307 $val = $match[1];
308 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
309 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
310 $stack->push($val);
311 $stack->push(1);
312 $stack->push('(');
313 $expecting_op = false;
314 } else { // it's a var w/ implicit multiplication
315 $val = $matches[1];
316 $output[] = $val;
318 } else { // it's a plain old var or num
319 $output[] = $val;
321 $index += strlen($val);
322 //===============
323 } elseif ($op == ')') {
324 //it could be only custom function with no params or general error
325 if ($stack->last() != '(' or $stack->last(2) != 1) return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
326 if (preg_match('/^('.self::$namepat.')\($/', $stack->last(3), $matches)) { // did we just close a function?
327 $stack->pop();// (
328 $stack->pop();// 1
329 $fn = $stack->pop();
330 $fnn = $matches[1]; // get the function name
331 $counts = $this->fc[$fnn];
332 if (!in_array(0, $counts)){
333 $a= new stdClass();
334 $a->expected = $this->fc[$fnn];
335 $a->given = 0;
336 return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
338 $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>0); // send function to output
339 $index++;
340 $expecting_op = true;
341 } else {
342 return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
344 //===============
345 } elseif (in_array($op, $ops) and !$expecting_op) { // miscellaneous error checking
346 return $this->trigger(get_string('unexpectedoperator', 'mathslib', $op));
347 } else { // I don't even want to know what you did to get here
348 return $this->trigger(get_string('anunexpectederroroccured', 'mathslib'));
350 if ($index == strlen($expr)) {
351 if (in_array($op, $ops)) { // did we end with an operator? bad.
352 return $this->trigger(get_string('operatorlacksoperand', 'mathslib', $op));
353 } else {
354 break;
357 while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
358 $index++; // into implicit multiplication if no operator is there)
362 while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
363 if ($op == '(') return $this->trigger(get_string('expectingaclosingbracket', 'mathslib')); // if there are (s on the stack, ()s were unbalanced
364 $output[] = $op;
366 return $output;
369 // evaluate postfix notation
370 function pfx($tokens, $vars = array()) {
372 if ($tokens == false) return false;
374 $stack = new EvalMathStack;
376 foreach ($tokens as $token) { // nice and easy
378 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
379 if (is_array($token)) { // it's a function!
380 $fnn = $token['fnn'];
381 $count = $token['argcount'];
382 if (in_array($fnn, $this->fb)) { // built-in function:
383 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
384 $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
385 if ($fnn == 'ln') $fnn = 'log';
386 eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
387 } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function
388 // get args
389 $args = array();
390 for ($i = $count-1; $i >= 0; $i--) {
391 if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
393 $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args));
394 if ($res === FALSE) {
395 return $this->trigger(get_string('internalerror', 'mathslib'));
397 $stack->push($res);
398 } elseif (array_key_exists($fnn, $this->f)) { // user function
399 // get args
400 $args = array();
401 for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
402 if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
404 $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
406 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
407 } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) {
408 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
409 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
410 switch ($token) {
411 case '+':
412 $stack->push($op1+$op2); break;
413 case '-':
414 $stack->push($op1-$op2); break;
415 case '*':
416 $stack->push($op1*$op2); break;
417 case '/':
418 if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib'));
419 $stack->push($op1/$op2); break;
420 case '^':
421 $stack->push(pow($op1, $op2)); break;
423 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
424 } elseif ($token == "_") {
425 $stack->push(-1*$stack->pop());
426 // if the token is a number or variable, push it on the stack
427 } else {
428 if (is_numeric($token)) {
429 $stack->push($token);
430 } elseif (array_key_exists($token, $this->v)) {
431 $stack->push($this->v[$token]);
432 } elseif (array_key_exists($token, $vars)) {
433 $stack->push($vars[$token]);
434 } else {
435 return $this->trigger(get_string('undefinedvariable', 'mathslib', $token));
439 // when we're out of tokens, the stack should have a single element, the final result
440 if ($stack->count != 1) return $this->trigger(get_string('internalerror', 'mathslib'));
441 return $stack->pop();
444 // trigger an error, but nicely, if need be
445 function trigger($msg) {
446 $this->last_error = $msg;
447 if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
448 return false;
453 // for internal use
454 class EvalMathStack {
456 var $stack = array();
457 var $count = 0;
459 function push($val) {
460 $this->stack[$this->count] = $val;
461 $this->count++;
464 function pop() {
465 if ($this->count > 0) {
466 $this->count--;
467 return $this->stack[$this->count];
469 return null;
472 function last($n=1) {
473 if ($this->count - $n >= 0) {
474 return $this->stack[$this->count-$n];
476 return null;
481 // spreadsheet functions emulation
482 class EvalMathFuncs {
484 static function average() {
485 $args = func_get_args();
486 return (call_user_func_array(array('self', 'sum'), $args) / count($args));
489 static function max() {
490 $args = func_get_args();
491 $res = array_pop($args);
492 foreach($args as $a) {
493 if ($res < $a) {
494 $res = $a;
497 return $res;
500 static function min() {
501 $args = func_get_args();
502 $res = array_pop($args);
503 foreach($args as $a) {
504 if ($res > $a) {
505 $res = $a;
508 return $res;
511 static function mod($op1, $op2) {
512 return $op1 % $op2;
515 static function pi() {
516 return pi();
519 static function power($op1, $op2) {
520 return pow($op1, $op2);
523 static function round($val, $precision = 0) {
524 return round($val, $precision);
527 static function sum() {
528 $args = func_get_args();
529 $res = 0;
530 foreach($args as $a) {
531 $res += $a;
533 return $res;
536 protected static $randomseed = null;
538 static function set_random_seed($randomseed) {
539 self::$randomseed = $randomseed;
542 static function get_random_seed() {
543 if (is_null(self::$randomseed)){
544 return microtime();
545 } else {
546 return self::$randomseed;
550 static function rand_int($min, $max){
551 if ($min >= $max) {
552 return false; //error
554 $noofchars = ceil(log($max + 1 - $min, '16'));
555 $md5string = md5(self::get_random_seed());
556 $stringoffset = 0;
557 do {
558 while (($stringoffset + $noofchars) > strlen($md5string)){
559 $md5string .= md5($md5string);
561 $randomno = hexdec(substr($md5string, $stringoffset, $noofchars));
562 $stringoffset += $noofchars;
563 } while (($min + $randomno) > $max);
564 return $min + $randomno;
567 static function rand_float() {
568 $randomvalues = unpack('v', md5(self::get_random_seed(), true));
569 return array_shift($randomvalues) / 65536;