ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
class.EvalMath.php
Go to the documentation of this file.
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 class EvalMath
90 {
91  public $suppress_errors = false;
92  public $last_error = null;
93 
94  public $v = array('e'=>2.71,'pi'=>3.14); // variables (and constants)
95  public $f = array(); // user-defined functions
96  public $vb = array('e', 'pi'); // constants
97  public $fb = array( // built-in functions
98  'sin','sinh','arcsin','asin','arcsinh','asinh',
99  'cos','cosh','arccos','acos','arccosh','acosh',
100  'tan','tanh','arctan','atan','arctanh','atanh',
101  'sqrt','abs','ln','log');
102  // mjansen-patch: begin
103  public function __construct()
104  {
105  // mjansen-patch: end
106  // make the variables a little more accurate
107  $this->v['pi'] = pi();
108  $this->v['exp'] = exp(1);
109  // PATCH BEGIN
110  $this->v['e'] = exp(1); // different result for exp(1) and e
111  $this->fb[] = 'exp'; // usage of php exp function in formula
112  // PATCH END
113  }
114 
115  public function e($expr)
116  {
117  return $this->evaluate($expr);
118  }
119 
120  public function evaluate($expr)
121  {
122  // convert exponential notation
123  $expr = preg_replace_callback(
124  "/(\\d{0,1})e(-{0,1}\\d+)/is",
125  function ($hit) {
126  return $hit[1] . ((strlen($hit[1])) ? '*' : '') . '10^(' . $hit[2] . ')';
127  },
128  $expr
129  );
130  // standard functionality
131  $this->last_error = null;
132  $expr = trim($expr);
133  if (substr($expr, -1, 1) == ';') {
134  $expr = substr($expr, 0, strlen($expr)-1);
135  } // strip semicolons at the end
136  //===============
137  // is it a variable assignment?
138  if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
139  if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
140  return $this->trigger("cannot assign to constant '$matches[1]'");
141  }
142  if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) {
143  return false;
144  } // get the result and make sure it's good
145  $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
146  return $this->v[$matches[1]]; // and return the resulting value
147  //===============
148  // is it a function assignment?
149  } elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
150  $fnn = $matches[1]; // get the function name
151  if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
152  return $this->trigger("cannot redefine built-in function '$matches[1]()'");
153  }
154  $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
155  if (($stack = $this->nfx($matches[3])) === false) {
156  return false;
157  } // see if it can be converted to postfix
158  for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
159  $token = $stack[$i];
160  if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
161  if (array_key_exists($token, $this->v)) {
162  $stack[$i] = $this->v[$token];
163  } else {
164  return $this->trigger("undefined variable '$token' in function definition");
165  }
166  }
167  }
168  $this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
169  return true;
170  //===============
171  } else {
172  return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
173  }
174  }
175 
176  public function vars()
177  {
178  $output = $this->v;
179  unset($output['pi']);
180  unset($output['e']);
181  return $output;
182  }
183 
184  public function funcs()
185  {
186  $output = array();
187  foreach ($this->f as $fnn=>$dat) {
188  $output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
189  }
190  return $output;
191  }
192 
193  //===================== HERE BE INTERNAL METHODS ====================\\
194 
195  // Convert infix to postfix notation
196  public function nfx($expr)
197  {
198  $index = 0;
199  $stack = new EvalMathStack;
200  $output = array(); // postfix form of expression, to be passed to pfx()
201  $expr = trim(strtolower($expr));
202 
203  $ops = array('+', '-', '*', '/', '^', '_');
204  $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
205  $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
206 
207  $expecting_op = false; // we use this in syntax-checking the expression
208  // and determining when a - is a negation
209 
210  if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
211  return $this->trigger("illegal character '{$matches[0]}'");
212  }
213 
214  while (1) { // 1 Infinite Loop ;)
215  $op = substr($expr, $index, 1); // get the first character at the current index
216  // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
217  $ex = preg_match('/^([01]+[bB]|[\da-fA-F]+[hH]|[a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
218  //===============
219  if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
220  $stack->push('_'); // put a negation on the stack
221  $index++;
222  } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
223  return $this->trigger("illegal character '_'"); // but not in the input expression
224  //===============
225  } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
226  if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
227  $op = '*';
228  $index--; // it's an implicit multiplication
229  }
230  // heart of the algorithm:
231  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])) {
232  $output[] = $stack->pop(); // pop stuff off the stack into the output
233  }
234  // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
235  $stack->push($op); // finally put OUR operator onto the stack
236  $index++;
237  $expecting_op = false;
238  //===============
239  } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
240  while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
241  if (is_null($o2)) {
242  return $this->trigger("unexpected ')'");
243  } else {
244  $output[] = $o2;
245  }
246  }
247  if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function?
248  $fnn = $matches[1]; // get the function name
249  $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
250  $output[] = $stack->pop(); // pop the function and push onto the output
251  if (in_array($fnn, $this->fb)) { // check the argument count
252  if ($arg_count > 1) {
253  return $this->trigger("too many arguments ($arg_count given, 1 expected)");
254  }
255  } elseif (array_key_exists($fnn, $this->f)) {
256  if ($arg_count != count($this->f[$fnn]['args'])) {
257  return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)");
258  }
259  } else { // did we somehow push a non-function on the stack? this should never happen
260  return $this->trigger("internal error");
261  }
262  }
263  $index++;
264  //===============
265  } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
266  while (($o2 = $stack->pop()) != '(') {
267  if (is_null($o2)) {
268  return $this->trigger("unexpected ','");
269  } // oops, never had a (
270  else {
271  $output[] = $o2;
272  } // pop the argument expression stuff and push onto the output
273  }
274  // make sure there was a function
275  if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) {
276  return $this->trigger("unexpected ','");
277  }
278  $stack->push($stack->pop()+1); // increment the argument count
279  $stack->push('('); // put the ( back on, we'll need to pop back to it again
280  $index++;
281  $expecting_op = false;
282  //===============
283  } elseif ($op == '(' and !$expecting_op) {
284  $stack->push('('); // that was easy
285  $index++;
286  $allow_neg = true;
287  //===============
288  } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
289  $expecting_op = true;
290  $val = $match[1];
291  if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
292  if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
293  $stack->push($val);
294  $stack->push(1);
295  $stack->push('(');
296  $expecting_op = false;
297  } else { // it's a var w/ implicit multiplication
298  $val = $matches[1];
299  $output[] = $val;
300  }
301  } else { // it's a plain old var or num
302  $output[] = $val;
303  }
304  $index += strlen($val);
305  //===============
306  } elseif ($op == ')') { // miscellaneous error checking
307  return $this->trigger("unexpected ')'");
308  } elseif (in_array($op, $ops) and !$expecting_op) {
309  return $this->trigger("unexpected operator '$op'");
310  } else { // I don't even want to know what you did to get here
311  return $this->trigger("an unexpected error occured");
312  }
313  if ($index == strlen($expr)) {
314  if (in_array($op, $ops)) { // did we end with an operator? bad.
315  return $this->trigger("operator '$op' lacks operand");
316  } else {
317  break;
318  }
319  }
320  while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
321  $index++; // into implicit multiplication if no operator is there)
322  }
323  }
324  while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
325  if ($op == '(') {
326  return $this->trigger("expecting ')'");
327  } // if there are (s on the stack, ()s were unbalanced
328  $output[] = $op;
329  }
330  return $output;
331  }
332 
333  // evaluate postfix notation
334  public function pfx($tokens, $vars = array())
335  {
336  if ($tokens == false) {
337  return false;
338  }
339 
340  $stack = new EvalMathStack;
341 
342  foreach ($tokens as $token) { // nice and easy
343  // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
344  if (in_array($token, array('+', '-', '*', '/', '^'))) {
345  if (is_null($op2 = $stack->pop())) {
346  return $this->trigger("internal error");
347  }
348  if (is_null($op1 = $stack->pop())) {
349  return $this->trigger("internal error");
350  }
351  include_once "class.ilMath.php";
352  switch ($token) {
353  case '+':
354  $stack->push(ilMath::_add($op1, $op2)); break;
355  case '-':
356  $stack->push(ilMath::_sub($op1, $op2)); break;
357  case '*':
358  $stack->push(ilMath::_mul($op1, $op2)); break;
359  case '/':
360  if ($op2 == 0) {
361  return $this->trigger("division by zero");
362  }
363  $stack->push(ilMath::_div($op1, $op2)); break;
364  case '^':
365  $stack->push(ilMath::_pow($op1, $op2)); break;
366  }
367  // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
368  } elseif ($token == "_") {
369  $stack->push(-1*$stack->pop());
370  // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
371  } elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function!
372  $fnn = $matches[1];
373  if (in_array($fnn, $this->fb)) { // built-in function:
374  if (is_null($op1 = $stack->pop())) {
375  return $this->trigger("internal error");
376  }
377  $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
378  if ($fnn == 'log') {
379  $fnn = 'log10';
380  } elseif ($fnn == 'ln') {
381  $fnn = 'log';
382  }
383 
384  $stack->push($fnn($op1)); // 'eval()' can be easily avoided here
385  } elseif (array_key_exists($fnn, $this->f)) { // user function
386  // get args
387  $args = array();
388  for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
389  if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) {
390  return $this->trigger("internal error");
391  }
392  }
393  $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
394  }
395  // if the token is a number or variable, push it on the stack
396  } else {
397  if (is_numeric($token)) {
398  $stack->push($token);
399  } elseif (($hex=$this->from_hexbin($token))!==false) {
400  $stack->push($hex);
401  } elseif (array_key_exists($token, $this->v)) {
402  $stack->push($this->v[$token]);
403  } elseif (array_key_exists($token, $vars)) {
404  $stack->push($vars[$token]);
405  } else {
406  return $this->trigger("undefined variable '$token'");
407  }
408  }
409  }
410  // when we're out of tokens, the stack should have a single element, the final result
411  if ($stack->count != 1) {
412  return $this->trigger("internal error");
413  }
414  return $stack->pop();
415  }
416 
417  // trigger an error, but nicely, if need be
418  public function trigger($msg)
419  {
420  $this->last_error = $msg;
421  if (!$this->suppress_errors) {
422  trigger_error($msg, E_USER_WARNING);
423  }
424  return false;
425  }
426 
427  // check if the token is a hex/bin number, and convert to decimal
428  // 1234h/0101010b are allowed
429  public function from_hexbin($token)
430  {
431  if (strtoupper(substr($token, -1, 1))=='H') {
432  return hexdec($token);
433  }
434  if (strtoupper(substr($token, -1, 1))=='B') {
435  return bindec($token);
436  }
437  return false;
438  }
439 }
440 
441 // for internal use
443 {
444  public $stack = array();
445  public $count = 0;
446 
447  public function push($val)
448  {
449  $this->stack[$this->count] = $val;
450  $this->count++;
451  }
452 
453  public function pop()
454  {
455  if ($this->count > 0) {
456  $this->count--;
457  return $this->stack[$this->count];
458  }
459  return null;
460  }
461 
462  public function last($n=1)
463  {
464  // mjansen-patch: begin
465  if (isset($this->stack[$this->count-$n])) {
466  return $this->stack[$this->count - $n];
467  }
468  return null;
469  // mjansen-patch: end
470  }
471 }
from_hexbin($token)
static _div($left_operand, $right_operand, $scale=50)
$index
Definition: metadata.php:60
static _pow($left_operand, $right_operand, $scale=50)
static _add($left_operand, $right_operand, $scale=50)
if(!is_dir( $entity_dir)) exit("Fatal Error ([A-Za-z0-9]+)\+" &#(? foreach( $entity_files as $file) $output
pfx($tokens, $vars=array())
$n
Definition: RandomTest.php:85
Create styles array
The data for the language used.
static _mul($left_operand, $right_operand, $scale=50)
$i
Definition: disco.tpl.php:19
evaluate($expr)
static _sub($left_operand, $right_operand, $scale=50)