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