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