1<?php
2
3/*
4================================================================================
5
6EvalMath - PHP Class to safely evaluate math expressions
7Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
8
9================================================================================
10
11NAME
12    EvalMath - safely evaluate math expressions
13
14SYNOPSIS
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
30DESCRIPTION
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
35METHODS
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
50PARAMETERS
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
58AUTHOR INFORMATION
59    Copyright 2005, Miles Kaufmann.
60
61LICENSE
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
89class 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
442class EvalMathStack
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}
472