MDL-51080 env: warn when running MariaDB w/ wrong dbtype
[moodle.git] / lib / evalmath / evalmath.class.php
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
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     ?>
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.
87 */
89 /**
90  * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
91  * skodak
92  *
93  */
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);
124         }
125         $this->allowimplicitmultiplication = $allowimplicitmultiplication;
126     }
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     }
138     function e($expr) {
139         return $this->evaluate($expr);
140     }
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     }
182     function vars() {
183         return $this->v;
184     }
186     function funcs() {
187         $output = array();
188         foreach ($this->f as $fnn=>$dat)
189             $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
190         return $output;
191     }
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     }
201     //===================== HERE BE INTERNAL METHODS ====================\\
203     // Convert infix to postfix notation
204     function nfx($expr) {
206         $index = 0;
207         $stack = new EvalMathStack;
208         $output = array(); // postfix form of expression, to be passed to pfx()
209         $expr = trim(strtolower($expr));
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
215         $expecting_op = false; // we use this in syntax-checking the expression
216                                // and determining when a - is a negation
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         }
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             }
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     }
372     // evaluate postfix notation
373     function pfx($tokens, $vars = array()) {
375         if ($tokens == false) return false;
377         $stack = new EvalMathStack;
379         foreach ($tokens as $token) { // nice and easy
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     }
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     }
456 // for internal use
457 class EvalMathStack {
459     var $stack = array();
460     var $count = 0;
462     function push($val) {
463         $this->stack[$this->count] = $val;
464         $this->count++;
465     }
467     function pop() {
468         if ($this->count > 0) {
469             $this->count--;
470             return $this->stack[$this->count];
471         }
472         return null;
473     }
475     function last($n=1) {
476         if ($this->count - $n >= 0) {
477             return $this->stack[$this->count-$n];
478         }
479         return null;
480     }
484 // spreadsheet functions emulation
485 class EvalMathFuncs {
487     static function average() {
488         $args = func_get_args();
489         return (call_user_func_array(array('self', 'sum'), $args) / count($args));
490     }
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     }
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     }
514     static function mod($op1, $op2) {
515         return $op1 % $op2;
516     }
518     static function pi() {
519         return pi();
520     }
522     static function power($op1, $op2) {
523         return pow($op1, $op2);
524     }
526     static function round($val, $precision = 0) {
527         return round($val, $precision);
528     }
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     }
539     protected static $randomseed = null;
541     static function set_random_seed($randomseed) {
542         self::$randomseed = $randomseed;
543     }
545     static function get_random_seed() {
546         if (is_null(self::$randomseed)){
547             return microtime();
548         } else {
549             return self::$randomseed;
550         }
551     }
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     }
570     static function rand_float() {
571         $randomvalues = unpack('v', md5(self::get_random_seed(), true));
572         return array_shift($randomvalues) / 65536;
573     }