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