Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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   * This class was modified to allow comparison operators (<, <=, ==, >=, >)
  93   * and synonyms functions (for the 'if' function). See MDL-14274 for more details.
  94   */
  95  
  96  class EvalMath {
  97  
  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_]*';
 100  
 101      var $suppress_errors = false;
 102      var $last_error = null;
 103  
 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');
 112  
 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), 'cond_and'=>array(-1), 'cond_or'=>array(-1));
 118      var $fcsynonyms = array('if' => 'ifthenelse', 'and' => 'cond_and', 'or' => 'cond_or');
 119  
 120      var $allowimplicitmultiplication;
 121  
 122      public function __construct($allowconstants = false, $allowimplicitmultiplication = false) {
 123          if ($allowconstants){
 124              $this->v['pi'] = pi();
 125              $this->v['e'] = exp(1);
 126          }
 127          $this->allowimplicitmultiplication = $allowimplicitmultiplication;
 128      }
 129  
 130      /**
 131       * Old syntax of class constructor. Deprecated in PHP7.
 132       *
 133       * @deprecated since Moodle 3.1
 134       */
 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);
 138      }
 139  
 140      function e($expr) {
 141          return $this->evaluate($expr);
 142      }
 143  
 144      function evaluate($expr) {
 145          $this->last_error = null;
 146          $expr = trim($expr);
 147          if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
 148          //===============
 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]));
 153              }
 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
 157          //===============
 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]));
 163              }
 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
 167                  $token = $stack[$i];
 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];
 171                      } else {
 172                          return $this->trigger(get_string('undefinedvariableinfunctiondefinition', 'mathslib', $token));
 173                      }
 174                  }
 175              }
 176              $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
 177              return true;
 178          //===============
 179          } else {
 180              return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
 181          }
 182      }
 183  
 184      function vars() {
 185          return $this->v;
 186      }
 187  
 188      function funcs() {
 189          $output = array();
 190          foreach ($this->f as $fnn=>$dat)
 191              $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
 192          return $output;
 193      }
 194  
 195      /**
 196       * @param string $name
 197       * @return boolean Is this a valid var or function name?
 198       */
 199      public static function is_valid_var_or_func_name($name){
 200          return preg_match('/'.self::$namepat.'$/iA', $name);
 201      }
 202  
 203      //===================== HERE BE INTERNAL METHODS ====================\\
 204  
 205      // Convert infix to postfix notation
 206      function nfx($expr) {
 207  
 208          $index = 0;
 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, '%' => 0); // right-associative operator?
 215          $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>3, '<'=>3, '<='=>3, '>='=>3, '=='=>3, '%'=>1); // operator precedence
 216  
 217          $expecting_op = false; // we use this in syntax-checking the expression
 218                                 // and determining when a - is a negation
 219  
 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]));
 222          }
 223  
 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
 230              }
 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);
 233              //===============
 234              if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
 235                  $stack->push('_'); // put a negation on the stack
 236                  $index++;
 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
 239              //===============
 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
 245                          $op = '*';
 246                          $index--;
 247                      }
 248                  }
 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
 252                  }
 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;
 257              //===============
 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;
 262                  }
 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)
 266                      $fn = $stack->pop();
 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
 269                          if($arg_count > 1) {
 270                              $a= new stdClass();
 271                              $a->expected = 1;
 272                              $a->given = $arg_count;
 273                              return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
 274                          }
 275                      } elseif ($this->get_native_function_name($fnn)) {
 276                          $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
 277  
 278                          $counts = $this->fc[$fnn];
 279                          if (in_array(-1, $counts) and $arg_count > 0) {}
 280                          elseif (!in_array($arg_count, $counts)) {
 281                              $a= new stdClass();
 282                              $a->expected = implode('/',$this->fc[$fnn]);
 283                              $a->given = $arg_count;
 284                              return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
 285                          }
 286                      } elseif (array_key_exists($fnn, $this->f)) {
 287                          if ($arg_count != count($this->f[$fnn]['args'])) {
 288                              $a= new stdClass();
 289                              $a->expected = count($this->f[$fnn]['args']);
 290                              $a->given = $arg_count;
 291                              return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
 292                          }
 293                      } else { // did we somehow push a non-function on the stack? this should never happen
 294                          return $this->trigger(get_string('internalerror', 'mathslib'));
 295                      }
 296                  }
 297                  $index++;
 298              //===============
 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
 303                  }
 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
 309                  $index++;
 310                  $expecting_op = false;
 311              //===============
 312              } elseif ($op == '(' and !$expecting_op) {
 313                  $stack->push('('); // that was easy
 314                  $index++;
 315                  $allow_neg = true;
 316              //===============
 317              } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
 318                  $expecting_op = true;
 319                  $val = $match[1];
 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
 324                          $stack->push($val);
 325                          $stack->push(1);
 326                          $stack->push('(');
 327                          $expecting_op = false;
 328                      } else { // it's a var w/ implicit multiplication
 329                          $val = $matches[1];
 330                          $output[] = $val;
 331                      }
 332                  } else { // it's a plain old var or num
 333                      $output[] = $val;
 334                  }
 335                  $index += strlen($val);
 336              //===============
 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?
 341                      $stack->pop();// (
 342                      $stack->pop();// 1
 343                      $fn = $stack->pop();
 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)){
 348                          $a= new stdClass();
 349                          $a->expected = $this->fc[$fnn];
 350                          $a->given = 0;
 351                          return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
 352                      }
 353                      $output[] = array('fn'=>$fn, 'fnn'=>$fnn, 'argcount'=>0); // send function to output
 354                      $index++;
 355                      $expecting_op = true;
 356                  } else {
 357                      return $this->trigger(get_string('unexpectedclosingbracket', 'mathslib'));
 358                  }
 359              //===============
 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'));
 364              }
 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));
 368                  } else {
 369                      break;
 370                  }
 371              }
 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)
 374              }
 375  
 376          }
 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
 379              $output[] = $op;
 380          }
 381          return $output;
 382      }
 383      /**
 384       *
 385       * @param string $fnn
 386       * @return string|boolean false if function name unknown.
 387       */
 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)) {
 392              return $fnn;
 393          } else {
 394              return false;
 395          }
 396      }
 397      // evaluate postfix notation
 398      function pfx($tokens, $vars = array()) {
 399  
 400          if ($tokens == false) return false;
 401  
 402          $stack = new EvalMathStack;
 403  
 404          foreach ($tokens as $token) { // nice and easy
 405  
 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.
 417                      // get args
 418                      $args = array();
 419                      for ($i = $count-1; $i >= 0; $i--) {
 420                          if (is_null($args[] = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
 421                      }
 422                      $res = call_user_func_array(array('EvalMathFuncs', $fnn), array_reverse($args));
 423                      if ($res === FALSE) {
 424                          return $this->trigger(get_string('internalerror', 'mathslib'));
 425                      }
 426                      $stack->push($res);
 427                  } elseif (array_key_exists($fnn, $this->f)) { // user function
 428                      // get args
 429                      $args = array();
 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'));
 432                      }
 433                      $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
 434                  }
 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'));
 439                  switch ($token) {
 440                      case '+':
 441                          $stack->push($op1+$op2); break;
 442                      case '-':
 443                          $stack->push($op1-$op2); break;
 444                      case '*':
 445                          $stack->push($op1*$op2); break;
 446                      case '/':
 447                          if ($op2 == 0) return $this->trigger(get_string('divisionbyzero', 'mathslib'));
 448                          $stack->push($op1/$op2); break;
 449                      case '^':
 450                          $stack->push(pow($op1, $op2)); break;
 451                      case '>':
 452                          $stack->push((int)($op1 > $op2)); break;
 453                      case '<':
 454                          $stack->push((int)($op1 < $op2)); break;
 455                      case '==':
 456                          $stack->push((int)($op1 == $op2)); break;
 457                      case '<=':
 458                          $stack->push((int)($op1 <= $op2)); break;
 459                      case '>=':
 460                          $stack->push((int)($op1 >= $op2)); break;
 461                      case '%':
 462                          $stack->push($op1%$op2); break;
 463                  }
 464              // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
 465              } elseif ($token == "_") {
 466                  $stack->push(-1*$stack->pop());
 467              // if the token is a number or variable, push it on the stack
 468              } else {
 469                  if (is_numeric($token)) {
 470                      $stack->push($token);
 471                  } elseif (array_key_exists($token, $this->v)) {
 472                      $stack->push($this->v[$token]);
 473                  } elseif (array_key_exists($token, $vars)) {
 474                      $stack->push($vars[$token]);
 475                  } else {
 476                      return $this->trigger(get_string('undefinedvariable', 'mathslib', $token));
 477                  }
 478              }
 479          }
 480          // when we're out of tokens, the stack should have a single element, the final result
 481          if ($stack->count != 1) return $this->trigger(get_string('internalerror', 'mathslib'));
 482          return $stack->pop();
 483      }
 484  
 485      // trigger an error, but nicely, if need be
 486      function trigger($msg) {
 487          $this->last_error = $msg;
 488          if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
 489          return false;
 490      }
 491  
 492  }
 493  
 494  // for internal use
 495  class EvalMathStack {
 496  
 497      var $stack = array();
 498      var $count = 0;
 499  
 500      function push($val) {
 501          $this->stack[$this->count] = $val;
 502          $this->count++;
 503      }
 504  
 505      function pop() {
 506          if ($this->count > 0) {
 507              $this->count--;
 508              return $this->stack[$this->count];
 509          }
 510          return null;
 511      }
 512  
 513      function last($n=1) {
 514          if ($this->count - $n >= 0) {
 515              return $this->stack[$this->count-$n];
 516          }
 517          return null;
 518      }
 519  }
 520  
 521  
 522  // spreadsheet functions emulation
 523  class EvalMathFuncs {
 524      /**
 525       * MDL-14274 new conditional function.
 526       * @param boolean $condition boolean for conditional.
 527       * @param variant $then value if condition is true.
 528       * @param unknown $else value if condition is false.
 529       * @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
 530       * @return unknown
 531       */
 532      static function ifthenelse($condition, $then, $else) {
 533          if ($condition == true) {
 534              return $then;
 535          } else {
 536              return $else;
 537          }
 538      }
 539  
 540      static function cond_and() {
 541          $args = func_get_args();
 542          foreach($args as $a) {
 543              if ($a == false) {
 544                  return 0;
 545              }
 546          }
 547          return 1;
 548      }
 549  
 550      static function cond_or() {
 551          $args = func_get_args();
 552          foreach($args as $a) {
 553              if($a == true) {
 554                  return 1;
 555              }
 556          }
 557          return 0;
 558      }
 559  
 560      static function average() {
 561          $args = func_get_args();
 562          return (call_user_func_array(array(self::class, 'sum'), $args) / count($args));
 563      }
 564  
 565      static function max() {
 566          $args = func_get_args();
 567          $res = array_pop($args);
 568          foreach($args as $a) {
 569              if ($res < $a) {
 570                  $res = $a;
 571              }
 572          }
 573          return $res;
 574      }
 575  
 576      static function min() {
 577          $args = func_get_args();
 578          $res = array_pop($args);
 579          foreach($args as $a) {
 580              if ($res > $a) {
 581                  $res = $a;
 582              }
 583          }
 584          return $res;
 585      }
 586  
 587      static function mod($op1, $op2) {
 588          return $op1 % $op2;
 589      }
 590  
 591      static function pi() {
 592          return pi();
 593      }
 594  
 595      static function power($op1, $op2) {
 596          return pow($op1, $op2);
 597      }
 598  
 599      static function round($val, $precision = 0) {
 600          return round($val, $precision);
 601      }
 602  
 603      static function sum() {
 604          $args = func_get_args();
 605          $res = 0;
 606          foreach($args as $a) {
 607             $res += $a;
 608          }
 609          return $res;
 610      }
 611  
 612      protected static $randomseed = null;
 613  
 614      static function set_random_seed($randomseed) {
 615          self::$randomseed = $randomseed;
 616      }
 617  
 618      static function get_random_seed() {
 619          if (is_null(self::$randomseed)){
 620              return microtime();
 621          } else {
 622              return self::$randomseed;
 623          }
 624      }
 625  
 626      static function rand_int($min, $max){
 627          if ($min >= $max) {
 628              return false; //error
 629          }
 630          $noofchars = ceil(log($max + 1 - $min, '16'));
 631          $md5string = md5(self::get_random_seed());
 632          $stringoffset = 0;
 633          do {
 634              while (($stringoffset + $noofchars) > strlen($md5string)){
 635                  $md5string .= md5($md5string);
 636              }
 637              $randomno = hexdec(substr($md5string, $stringoffset, $noofchars));
 638              $stringoffset += $noofchars;
 639          } while (($min + $randomno) > $max);
 640          return $min + $randomno;
 641      }
 642  
 643      static function rand_float() {
 644          $randomvalues = unpack('v', md5(self::get_random_seed(), true));
 645          return array_shift($randomvalues) / 65536;
 646      }
 647  }