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