4 * Expression - PHP Class to safely evaluate math expressions
6 * Copyright 2005 Miles Kaufmann <http://www.twmagic.com/>
7 * Copyright 2012 - 2015 Johan Falk <http://magisterfalk.wordpress.com/>
8 * Copyright 2015 Colin Kiegel <http://colin-kiegel.github.io/>
9 * Copyright 2016 Jakub Jankiewicz <http://jcubic.pl/>
10 * Copyright 2016 Константин <https://github.com/optimistex>
11 * Copyright 2016 Stéphane-Eymeric Bredthauer - TrivaDev
14 * Expression - safely evaluate math and boolean expressions
18 * include('expression.php');
19 * $e = new Expression();
20 * // basic evaluation:
21 * $result = $e->evaluate('2+2');
22 * // supports: order of operation; parentheses; negation; built-in functions
23 * $result = $e->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
24 * // create your own variables
25 * $e->evaluate('a = e^(ln(pi))');
27 * $e->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
28 * // and then use them
29 * $result = $e->evaluate('3*f(42,a)');
33 * Use the Expression class when you want to evaluate mathematical or boolean
34 * expressions from untrusted sources. You can define your own variables and
35 * functions, which are stored in the object. Try it, it's fun!
37 * Based on http://www.phpclasses.org/browse/file/11680.html, cred to Miles Kaufmann
41 * Evaluates the expression and returns the result. If an error occurs,
42 * prints a warning and returns false. If $expr is a function assignment,
43 * returns true on success.
46 * A synonym for $e->evaluate().
49 * Returns an associative array of all user-defined variables and values.
52 * Returns an array of all user-defined functions.
56 * Set to true to turn off warnings when evaluating expressions
59 * If the last evaluation failed, contains a string describing the error.
60 * (Useful when suppress_errors is on).
62 * LICENSE (BSD 3 Clause)
63 * Redistribution and use in source and binary forms, with or without
64 * modification, are permitted provided that the following conditions are
67 * 1. Redistributions of source code must retain the above copyright
68 * notice, this list of conditions and the following disclaimer.
69 * 2. Redistributions in binary form must reproduce the above copyright
70 * notice, this list of conditions and the following disclaimer in the
71 * documentation and/or other materials provided with the distribution.
72 * 3. The name of the author may not be used to endorse or promote
73 * products derived from this software without specific prior written
76 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
77 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
78 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
79 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
80 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
81 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
82 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
83 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
84 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
85 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
86 * POSSIBILITY OF SUCH DAMAGE.
91 var $suppress_errors = false;
92 var $last_error = null;
93 var $v = array ('e' => 2.71, 'pi' => 3.14); // variables (and constants)
94 var $f = array (); // user-defined functions
95 var $vb = array ('e', 'pi'); // constants
96 var $fb = array ( // built-in functions
97 'sin', 'sinh', 'arcsin', 'asin', 'arcsinh', 'asinh',
98 'cos', 'cosh', 'arccos', 'acos', 'arccosh', 'acosh',
99 'tan', 'tanh', 'arctan', 'atan', 'arctanh', 'atanh',
100 'sqrt', 'abs', 'ln', 'log');
101 var $functions = array (); // function defined outside of Expression
102 function __construct() {
103 // make the variables a little more accurate
104 $this->v ['pi'] = pi ();
105 $this->v ['e'] = exp ( 1 );
108 return $this->evaluate ( $expr );
110 function evaluate($expr) {
111 $this->last_error = null;
112 $expr = trim ( $expr );
113 if (substr ( $expr, - 1, 1 ) == ';') {
114 $expr = substr ( $expr, 0, strlen ( $expr ) - 1 ); // strip semicolons at the end
117 // is it a variable assignment?
118 if (preg_match ( '/^\s*([a-z]\w*)\s*=(?!~|=)\s*(.+)$/', $expr, $matches )) {
119 if (in_array ( $matches [1], $this->vb )) { // make sure we're not assigning to a constant
120 return $this->trigger ( "cannot assign to constant '$matches[1]'" );
122 $tmp = $this->pfx ( $this->nfx ( $matches [2] ) );
123 $this->v [$matches [1]] = $tmp; // if so, stick it in the variable array
124 return $this->v [$matches [1]]; // and return the resulting value
126 // is it a function assignment?
127 } elseif (preg_match ( '/^\s*([a-z]\w*)\s*\((?:\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*)?\)\s*=(?!~|=)\s*(.+)$/', $expr, $matches )) {
128 $fnn = $matches [1]; // get the function name
129 if (in_array ( $matches [1], $this->fb )) { // make sure it isn't built in
130 return $this->trigger ( "cannot redefine built-in function '$matches[1]()'" );
132 if ($matches [2] != "") {
133 $args = explode ( ",", preg_replace ( "/\s+/", "", $matches [2] ) ); // get the arguments
137 if (($stack = $this->nfx ( $matches [3] )) === false) {
138 return false; // see if it can be converted to postfix
140 for($i = 0; $i < count ( $stack ); $i ++) { // freeze the state of the non-argument variables
141 $token = $stack [$i];
142 if (preg_match ( '/^[a-z]\w*$/', $token ) and ! in_array ( $token, $args )) {
143 if (array_key_exists ( $token, $this->v )) {
144 $stack [$i] = $this->v [$token];
146 return $this->trigger ( "undefined variable '$token' in function definition" );
150 $this->f [$fnn] = array (
157 return $this->pfx ( $this->nfx ( $expr ) ); // straight up evaluation, woo
162 unset ( $output ['pi'] );
163 unset ( $output ['e'] );
168 foreach ( $this->f as $fnn => $dat ) {
169 $output [] = $fnn . '(' . implode ( ',', $dat ['args'] ) . ')';
174 // ===================== HERE BE INTERNAL METHODS ====================\\
176 // Convert infix to postfix notation
177 function nfx($expr) {
179 $stack = new ExpressionStack ();
180 $output = array (); // postfix form of expression, to be passed to pfx()
181 $expr = trim ($expr);
183 $ops = array ('+', '-', '*', '/', '^', '_', '%', '>', '<', '>=', '<=', '==', '!=', '=~', '&&', '||', '!', '?', ':', '?:');
184 $ops_r = array ('+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1, '_' => 0, '%' => 0, '>' => 0, '<' => 0, '>=' => 0, '<=' => 0, '==' => 0, '!=' => 0, '=~' => 0, '&&' => 0, '||' => 0, '!' => 0, '?' => 1, ':' => 0, '?:' => 0); // right-associative operator?
185 // $ops_p = array('+'=>4,'-'=>4,'*'=>4,'/'=>4,'_'=>4,'%'=>4,'^'=>5,'>'=>2,'<'=>2,
186 // '>='=>2,'<='=>2,'=='=>2,'!='=>2,'=~'=>2,'&&'=>1,'||'=>1,'!'=>5); // operator precedence
190 '&&' => 2, '||' => 2,
191 '>' => 3, '<' => 3, '>=' => 3, '<=' => 3, '==' => 3, '!=' => 3, '=~' => 3,
193 '*' => 5, '/' => 5, '_' => 5, '%' => 5,
194 '^' => 6, '!' => 6); // operator precedence
195 $expecting_op = false; // we use this in syntax-checking the expression
196 // and determining when a - is a negation
199 * we allow all characters because of strings
200 * if (preg_match("%[^\w\s+*^\/()\.,-<>=&~|!\"\\\\/]%", $expr, $matches)) { // make sure the characters are all good
201 * return $this->trigger("illegal character '{$matches[0]}'");
204 $first_argument = false;
205 while ( 1 ) { // 1 Infinite Loop ;)
206 $op = substr ( $expr, $index, 2 ); // get the first two characters at the current index
207 if (preg_match ( "/^[+\-*\/^_\"<>=%(){\[!~,?:](?!=|~)/", $op ) || preg_match ( "/\w/", $op )) {
208 // fix $op if it should have one character
209 $op = substr ( $expr, $index, 1 );
211 $single_str = '(?<!\\\\)"(?:(?:(?<!\\\\)(?:\\\\{2})*\\\\)"|[^"])*(?<![^\\\\]\\\\)"';
212 $double_str = "(?<!\\\\)'(?:(?:(?<!\\\\)(?:\\\\{2})*\\\\)'|[^'])*(?<![^\\\\]\\\\)'";
213 $json = '[\[{](?>"(?:[^"]|\\\\")*"|[^[{\]}]|(?1))*[\]}]';
214 $number = '[\d.]+e\d+|\d+(?:\.\d*)?|\.\d+';
215 $name = '[a-z]\w*\(?|\\$\w+';
216 $parenthesis = '\\(';
217 // find out if we're currently at the beginning of a number/string/object/array/variable/function/parenthesis/operand
218 $ex = preg_match ( "%^($single_str|$double_str|$json|$name|$number|$parenthesis)%", substr ( $expr, $index ), $match );
220 if ($op == '[' && $expecting_op && $ex) {
221 if (! preg_match ( "/^\[(.*)\]$/", $match [1], $matches )) {
222 return $this->trigger ( "invalid array access" );
224 $stack->push ( '[' );
225 $stack->push ( $matches [1] );
226 $index += strlen ( $match [1] );
227 // } elseif ($op == '!' && !$expecting_op) {
228 // $stack->push('!'); // put a negation on the stack
230 } elseif ($op == '-' and ! $expecting_op) { // is it a negation instead of a minus?
231 $stack->push ( '_' ); // put a negation on the stack
233 } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
234 return $this->trigger ( "illegal character '_'" ); // but not in the input expression
235 } elseif (((in_array ( $op, $ops ) or $ex) and $expecting_op) or in_array ( $op, $ops ) and ! $expecting_op) {
236 // are we putting an operator on the stack?
237 if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
239 $index --; // it's an implicit multiplication
241 // heart of the algorithm:
242 $o2 = $stack->last ();
243 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]) ) {
244 $pop = $stack->pop ();
246 $output [] = $pop; // pop stuff off the stack into the output
252 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
253 $stack->push ( $op ); // finally put OUR operator onto the stack
255 if (strlen ( $op ) == 2 && $op != '?:') {
258 $expecting_op = false;
260 } elseif ($op == ')' and $expecting_op || ! $ex) { // ready to close a parenthesis?
261 while ( ($o2 = $stack->pop ()) != '(' ) { // pop off the stack back to the last (
262 if (is_null ( $o2 )) {
263 return $this->trigger ( "unexpected ')'" );
268 if (preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) { // did we just close a function?
269 $fnn = $matches [1]; // get the function name
270 $arg_count = $stack->pop (); // see how many arguments there were (cleverly stored on the stack, thank you)
271 $pop = $stack->pop();
272 $output [] = $pop; // pop the function and push onto the output
273 if (in_array ( $fnn, $this->fb )) { // check the argument count
274 if ($arg_count > 1) {
275 return $this->trigger ( "too many arguments ($arg_count given, 1 expected)" );
277 } elseif (array_key_exists ( $fnn, $this->f )) {
278 if ($arg_count != count ( $this->f [$fnn] ['args'] )) {
279 return $this->trigger ( "wrong number of arguments ($arg_count given, " . count ( $this->f [$fnn] ['args'] ) . " expected) " . json_encode ( $this->f [$fnn] ['args'] ) );
281 } elseif (array_key_exists ( $fnn, $this->functions )) {
282 $func_reflection = new ReflectionFunction ( $this->functions [$fnn] );
283 $count = $func_reflection->getNumberOfParameters ();
284 if ($arg_count != $count) {
285 return $this->trigger ( "wrong number of arguments ($arg_count given, " . $count . " expected)" );
287 } else { // did we somehow push a non-function on the stack? this should never happen
288 return $this->trigger ( "internal error" );
293 } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
294 while ( ($o2 = $stack->pop ()) != '(' ) {
295 if (is_null ( $o2 )) {
296 return $this->trigger ( "unexpected ','" ); // oops, never had a (
298 $output [] = $o2; // pop the argument expression stuff and push onto the output
301 // make sure there was a function
302 if (! preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) {
303 return $this->trigger ( "unexpected ','" );
305 if ($first_argument) {
306 $first_argument = false;
308 $pop = $stack->pop ();
309 $stack->push ( $pop + 1 ); // increment the argument count
311 $stack->push ( '(' ); // put the ( back on, we'll need to pop back to it again
313 $expecting_op = false;
315 } elseif ($op == '(' and ! $expecting_op) {
316 $stack->push ( '(' ); // that was easy
320 } elseif ($ex and ! $expecting_op) { // do we now have a function/variable/number?
321 $expecting_op = true;
323 if ($op == '[' || $op == "{" || preg_match ( "/null|true|false/", $match [1] )) {
325 } elseif (preg_match ( "/^([a-z]\w*)\($/", $val, $matches )) { // may be func, or variable w/ implicit multiplication against parentheses...
326 if (in_array ( $matches [1], $this->fb ) or array_key_exists ( $matches [1], $this->f ) or array_key_exists ( $matches [1], $this->functions )) { // it's a func
327 $stack->push ( $val );
329 $stack->push ( '(' );
330 $expecting_op = false;
331 } else { // it's a var w/ implicit multiplication
335 } else { // it's a plain old var or num
337 if (preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 3 ) )) {
338 $first_argument = true;
339 while ( ($o2 = $stack->pop ()) != '(' ) {
340 if (is_null ( $o2 )) {
341 return $this->trigger ( "unexpected error" ); // oops, never had a (
343 $output [] = $o2; // pop the argument expression stuff and push onto the output
346 // make sure there was a function
347 if (! preg_match ( "/^([a-z]\w*)\($/", $stack->last ( 2 ), $matches )) {
348 return $this->trigger ( "unexpected error" );
350 $pop = $stack->pop ();
351 $stack->push ( $pop + 1 ); // increment the argument count
352 $stack->push ( '(' ); // put the ( back on, we'll need to pop back to it again
355 $index += strlen ( $val );
357 } elseif ($op == ')') { // miscellaneous error checking
358 return $this->trigger ( "unexpected ')'" );
359 } elseif (in_array ( $op, $ops ) and ! $expecting_op) {
360 return $this->trigger ( "unexpected operator '$op'" );
361 } else { // I don't even want to know what you did to get here
362 return $this->trigger ( "an unexpected error occured " . json_encode ( $op ) . " " . json_encode ( $match ) . " " . ($ex ? 'true' : 'false') . " " . $expr );
364 if ($index == strlen ( $expr )) {
365 if (in_array ( $op, $ops )) { // did we end with an operator? bad.
366 return $this->trigger ( "operator '$op' lacks operand" );
371 while ( substr ( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace
372 $index ++; // into implicit multiplication if no operator is there)
375 while ( ! is_null ( $op = $stack->pop () ) ) { // pop everything off the stack and push onto output
377 return $this->trigger ( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced
384 // evaluate postfix notation
385 function pfx($tokens, $vars = array()) {
386 $binaryOperator = array ('+', '-', '*', '/', '^', '<', '>', '<=', '>=', '==', '&&', '||', '!=', '=~', '%');
387 if ($tokens == false) {
390 $stack = new ExpressionStack ();
391 foreach ( $tokens as $token ) { // nice and easy
392 if ($token == '?:') {
393 $op1 = $stack->pop ();
394 $op2 = $stack->pop ();
395 $op3 = $stack->pop ();
397 $stack->push ( $op2 );
399 $stack->push ( $op1 );
401 // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
402 } elseif (in_array ( $token, $binaryOperator)) {
404 $op2 = $stack->pop ();
405 $op1 = $stack->pop ();
408 if (is_string ( $op1 ) || is_string ( $op2 )) {
409 $stack->push ( ( string ) $op1 . ( string ) $op2 );
411 $stack->push ( $op1 + $op2 );
415 $stack->push ( $op1 - $op2 );
418 $stack->push ( $op1 * $op2 );
422 return $this->trigger ( "division by zero" );
424 $stack->push ( $op1 / $op2 );
427 $stack->push ( $op1 % $op2 );
430 $stack->push ( pow ( $op1, $op2 ) );
433 $stack->push ( $op1 > $op2 );
436 $stack->push ( $op1 < $op2 );
439 $stack->push ( $op1 >= $op2 );
442 $stack->push ( $op1 <= $op2 );
445 if (is_array ( $op1 ) && is_array ( $op2 )) {
446 $stack->push ( json_encode ( $op1 ) == json_encode ( $op2 ) );
448 $stack->push ( $op1 == $op2 );
452 if (is_array ( $op1 ) && is_array ( $op2 )) {
453 $stack->push ( json_encode ( $op1 ) != json_encode ( $op2 ) );
455 $stack->push ( $op1 != $op2 );
459 $value = @preg_match ( $op2, $op1, $match );
461 if (! is_int ( $value )) {
462 return $this->trigger ( "Invalid regex " . json_encode ( $op2 ) );
464 $stack->push ( $value );
465 for($i = 0; $i < count ( $match ); $i ++) {
466 $this->v ['$' . $i] = $match [$i];
470 $stack->push ( $op1 ? $op2 : $op1 );
473 $stack->push ( $op1 ? $op1 : $op2 );
476 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
477 } elseif ($token == '!') {
479 $stack->push ( ! $stack->pop () );
480 } elseif ($token == '[') {
482 $selector = $stack->pop ();
483 $object = $stack->pop ();
484 if (is_object ( $object )) {
485 $stack->push ( $object->$selector );
486 } elseif (is_array ( $object )) {
487 $stack->push ( $object [$selector] );
489 return $this->trigger ( "invalid object for selector" );
491 } elseif ($token == "_") {
492 $stack->push ( - 1 * $stack->pop () );
493 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
494 } elseif (preg_match ( "/^([a-z]\w*)\($/", $token, $matches )) { // it's a function!
496 if (in_array ( $fnn, $this->fb )) { // built-in function:
497 if (is_null ( $op1 = $stack->pop () )) {
498 return $this->trigger ( "internal error" );
500 $fnn = preg_replace ( "/^arc/", "a", $fnn ); // for the 'arc' trig synonyms
504 $stack->push ( $fnn ( $op1 ) ); // perfectly safe variable function call
505 } elseif (array_key_exists ( $fnn, $this->f )) { // user function
508 for($i = count ( $this->f [$fnn] ['args'] ) - 1; $i >= 0; $i --) {
509 if ($stack->isEmpty ()) {
510 return $this->trigger ( "internal error " . $fnn . " " . json_encode ( $this->f [$fnn] ['args'] ) );
512 $args [$this->f [$fnn] ['args'] [$i]] = $stack->pop ();
514 $stack->push ( $this->pfx ( $this->f [$fnn] ['func'], $args ) ); // yay... recursion!!!!
515 } elseif (array_key_exists ( $fnn, $this->functions )) {
516 $reflection = new ReflectionFunction ( $this->functions [$fnn] );
517 $count = $reflection->getNumberOfParameters ();
518 for($i = $count - 1; $i >= 0; $i --) {
519 if ($stack->isEmpty ()) {
520 return $this->trigger ( "internal error" );
522 $args [] = $stack->pop ();
524 $args = array_reverse($args);
525 $stack->push ( $reflection->invokeArgs ( $args ) );
527 // if the token is a number or variable, push it on the stack
529 if (preg_match ( '/^([\[{](?>"(?:[^"]|\\")*"|[^[{\]}]|(?1))*[\]}])$/', $token ) || preg_match ( "/^(null|true|false)$/", $token )) { // json
530 // return $this->trigger("invalid json " . $token);
531 if ($token == 'null') {
533 } elseif ($token == 'true') {
535 } elseif ($token == 'false') {
538 $value = json_decode ( $token );
539 if ($value == null) {
540 return $this->trigger ( "invalid json " . $token );
543 $stack->push ( $value );
544 } elseif (is_numeric ( $token )) {
545 $stack->push ( 0 + $token );
546 } else if (preg_match ( "/^['\\\"](.*)['\\\"]$/", $token )) {
547 $stack->push ( json_decode ( preg_replace_callback ( "/^['\\\"](.*)['\\\"]$/", function ($matches) {
548 $m = array ("/\\\\'/", '/(?<!\\\\)"/');
549 $r = array ("'", '\\"' );
550 return '"' . preg_replace ( $m, $r, $matches [1] ) . '"';
552 } elseif (array_key_exists ( $token, $this->v )) {
553 $stack->push ( $this->v [$token] );
554 } elseif (array_key_exists ( $token, $vars )) {
555 $stack->push ( $vars [$token] );
557 return $this->trigger ( "undefined variable '$token'" );
561 // when we're out of tokens, the stack should have a single element, the final result
562 if ($stack->count != 1) {
563 return $this->trigger ( "internal error" );
565 return $stack->pop ();
568 // trigger an error, but nicely, if need be
569 function trigger($msg) {
570 $this->last_error = $msg;
571 if (! $this->suppress_errors) {
572 trigger_error ( $msg, E_USER_WARNING );
579 class ExpressionStack {
580 var $stack = array ();
582 function push($val) {
583 $this->stack [$this->count] = $val;
587 if ($this->count > 0) {
589 return $this->stack [$this->count];
594 return empty ( $this->stack );
596 function last($n = 1) {
597 if (isset ( $this->stack [$this->count - $n] )) {
598 return $this->stack [$this->count - $n];