├── .gitignore ├── composer.json ├── src ├── Stack.php └── EvalMath.php ├── LICENSE ├── README.md └── tests └── EvalMathTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /composer.lock 4 | .project 5 | .settings 6 | .buildpath -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "webit/eval-math", 3 | "description" : "EvalMath", 4 | "type" : "library", 5 | "keywords" : [ 6 | "EvalMath" 7 | ], 8 | "require": { 9 | "php": ">=5.3" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "~4.2" 13 | }, 14 | "autoload" : { 15 | "psr-4" : { 16 | "Webit\\Util\\EvalMath\\" : "src/" 17 | } 18 | }, 19 | "extra": { 20 | "branch-alias": { 21 | "dev-master": "1.x-dev" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Stack.php: -------------------------------------------------------------------------------- 1 | stack[$this->count] = $val; 22 | $this->count++; 23 | } 24 | 25 | public function pop() 26 | { 27 | if ($this->count > 0) { 28 | $this->count--; 29 | return $this->stack[$this->count]; 30 | } 31 | 32 | return null; 33 | } 34 | 35 | public function last($n=1) 36 | { 37 | $key = $this->count - $n; 38 | 39 | return array_key_exists($key,$this->stack) ? $this->stack[$key] : null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | 1 Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. The name of the author may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 23 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 24 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Composer/Packagist version of EvalMath by Miles Kaufman 2 | Copyright (C) 2005 Miles Kaufmann 3 | NAME 4 | ---- 5 | EvalMath - safely evaluate math expressions 6 | 7 | DESCRIPTION 8 | ----------- 9 | Use the EvalMath class when you want to evaluate mathematical expressions 10 | from untrusted sources. You can define your own variables and functions, 11 | which are stored in the object. Try it, it's fun! 12 | 13 | SYNOPSIS 14 | -------- 15 | `$m = new EvalMath;` 16 | 17 | `// basic evaluation:` 18 | `$result = $m->evaluate('2+2');` 19 | 20 | `// supports: order of operation; parentheses; negation; built-in functions` 21 | `$result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');` 22 | 23 | `// create your own variables` 24 | `$m->evaluate('a = e^(ln(pi))');` 25 | 26 | `// or functions` 27 | `$m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');` 28 | 29 | `// and then use them` 30 | `$result = $m->evaluate('3*f(42,a)');` 31 | 32 | METHODS 33 | ------- 34 | `$m->evaluate($expr)` 35 | Evaluates the expression and returns the result. If an error occurs, 36 | prints a warning and returns false. If $expr is a function assignment, 37 | returns true on success. 38 | 39 | `$m->e($expr)` 40 | A synonym for $m->evaluate(). 41 | 42 | `$m->vars()` 43 | Returns an associative array of all user-defined variables and values. 44 | 45 | `$m->funcs()` 46 | Returns an array of all user-defined functions. 47 | 48 | PARAMETERS 49 | ---------- 50 | `$m->suppress_errors` 51 | Set to true to turn off warnings when evaluating expressions 52 | 53 | `$m->last_error` 54 | If the last evaluation failed, contains a string describing the error. 55 | (Useful when suppress_errors is on). 56 | -------------------------------------------------------------------------------- /tests/EvalMathTest.php: -------------------------------------------------------------------------------- 1 | 6 | * Created on 02 12, 2016, 17:17 7 | * Copyright (C) 8x8 8 | */ 9 | 10 | namespace Webit\Util\EvalMath\Tests; 11 | 12 | use Webit\Util\EvalMath\EvalMath; 13 | 14 | class EvalMathTest extends \PHPUnit_Framework_TestCase 15 | { 16 | /** 17 | * @var EvalMath 18 | */ 19 | private $evalMath; 20 | 21 | protected function setUp() 22 | { 23 | $this->evalMath = new EvalMath(); 24 | } 25 | 26 | /** 27 | * @test 28 | * @dataProvider moduloOperatorData 29 | */ 30 | public function shouldSupportModuloOperator($formula, $values, $expectedResult) 31 | { 32 | foreach ($values as $k => $v) { 33 | $this->evalMath->v[$k] = $v; 34 | } 35 | 36 | $this->assertEquals($expectedResult, $this->evalMath->evaluate($formula)); 37 | } 38 | 39 | public function moduloOperatorData() 40 | { 41 | return array( 42 | array( 43 | 'a%b', // 9%3 => 0 44 | array('a' => 9, 'b' => 3), 45 | 0 46 | ), 47 | array( 48 | 'a%b', // 10%3 => 1 49 | array('a' => 10, 'b' => 3), 50 | 1 51 | ), 52 | array( 53 | '10-a%(b+c*d)', // 10-10%(7-2*2) => 9 54 | array('a' => '10', 'b' => 7, 'c'=> -2, 'd' => 2), 55 | 9 56 | ) 57 | ); 58 | } 59 | 60 | /** 61 | * @test 62 | * @dataProvider doubleMinusData 63 | */ 64 | public function shouldConsiderDoubleMinusAsPlus($formula, $values, $expectedResult) 65 | { 66 | foreach ($values as $k => $v) { 67 | $this->evalMath->v[$k] = $v; 68 | } 69 | 70 | $this->assertEquals( 71 | $expectedResult, 72 | $this->evalMath->evaluate($formula) 73 | ); 74 | } 75 | 76 | public function doubleMinusData() 77 | { 78 | return array( 79 | array( 80 | 'a+b*c--d', // 1+2*3--4 => 1+6+4 => 11 81 | array( 82 | 'a' => 1, 83 | 'b' => 2, 84 | 'c' => 3, 85 | 'd' => 4 86 | ), 87 | 11 88 | ), 89 | array( 90 | 'a+b*c--d', // 1+2*3---4 => 1+6-4 => 3 91 | array( 92 | 'a' => 1, 93 | 'b' => 2, 94 | 'c' => 3, 95 | 'd' => -4 96 | ), 97 | 3 98 | ) 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/EvalMath.php: -------------------------------------------------------------------------------- 1 | 2.71,'pi'=>3.14); // variables (and constants) 23 | 24 | /** 25 | * @var array 26 | */ 27 | public $f = array(); // user-defined functions 28 | 29 | /** 30 | * @var array 31 | */ 32 | public $vb = array('e', 'pi'); // constants 33 | 34 | /** 35 | * @var array 36 | */ 37 | public $fb = array( // built-in functions 38 | 'sin','sinh','arcsin','asin','arcsinh','asinh', 39 | 'cos','cosh','arccos','acos','arccosh','acosh', 40 | 'tan','tanh','arctan','atan','arctanh','atanh', 41 | 'sqrt','abs','ln','log' 42 | ); 43 | 44 | public function __construct() 45 | { 46 | // make the variables a little more accurate 47 | $this->v['pi'] = pi(); 48 | $this->v['e'] = exp(1); 49 | } 50 | 51 | /** 52 | * @param string $expr 53 | * @return mixed 54 | */ 55 | public function e($expr) 56 | { 57 | return $this->evaluate($expr); 58 | } 59 | 60 | /** 61 | * @param string $expr 62 | * @return mixed 63 | */ 64 | public function evaluate($expr) 65 | { 66 | $this->last_error = null; 67 | $expr = trim($expr); 68 | if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end 69 | //=============== 70 | // is it a variable assignment? 71 | if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) { 72 | if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant 73 | return $this->trigger("cannot assign to constant '$matches[1]'"); 74 | } 75 | if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good 76 | $this->v[$matches[1]] = $tmp; // if so, stick it in the variable array 77 | return $this->v[$matches[1]]; // and return the resulting value 78 | //=============== 79 | // is it a function assignment? 80 | } elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) { 81 | $fnn = $matches[1]; // get the function name 82 | if (in_array($matches[1], $this->fb)) { // make sure it isn't built in 83 | return $this->trigger("cannot redefine built-in function '$matches[1]()'"); 84 | } 85 | $args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments 86 | if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix 87 | for ($i = 0; $iv)) { 91 | $stack[$i] = $this->v[$token]; 92 | } else { 93 | return $this->trigger("undefined variable '$token' in function definition"); 94 | } 95 | } 96 | } 97 | $this->f[$fnn] = array('args'=>$args, 'func'=>$stack); 98 | return true; 99 | //=============== 100 | } else { 101 | return $this->pfx($this->nfx($expr)); // straight up evaluation, woo 102 | } 103 | } 104 | 105 | /** 106 | * @return array 107 | */ 108 | public function vars() 109 | { 110 | $output = $this->v; 111 | unset($output['pi']); 112 | unset($output['e']); 113 | return $output; 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function funcs() 120 | { 121 | $output = array(); 122 | foreach ($this->f as $fnn=>$dat) 123 | $output[] = $fnn . '(' . implode(',', $dat['args']) . ')'; 124 | 125 | return $output; 126 | } 127 | 128 | //===================== HERE BE INTERNAL METHODS ====================\\ 129 | 130 | // Convert infix to postfix notation 131 | public function nfx($expr) 132 | { 133 | 134 | $index = 0; 135 | $stack = new Stack; 136 | $output = array(); // postfix form of expression, to be passed to pfx() 137 | // $expr = trim(strtolower($expr)); 138 | $expr = trim($expr); 139 | 140 | $ops = array('+', '-', '*', '/', '^', '_', '%'); 141 | $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1, '%' => 0); // right-associative operator? 142 | $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '%'=>1); // operator precedence 143 | 144 | $expecting_op = false; // we use this in syntax-checking the expression 145 | // and determining when a - is a negation 146 | 147 | if (preg_match('/[^\%\w\s+*^\/()\.,-]/', $expr, $matches)) { // make sure the characters are all good 148 | return $this->trigger("illegal character '{$matches[0]}'"); 149 | } 150 | 151 | while(1) { // 1 Infinite Loop ;) 152 | $op = substr($expr, $index, 1); // get the first character at the current index 153 | // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand 154 | $ex = preg_match('/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match); 155 | //=============== 156 | if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus? 157 | $stack->push('_'); // put a negation on the stack 158 | $index++; 159 | } elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack 160 | return $this->trigger("illegal character '_'"); // but not in the input expression 161 | //=============== 162 | } elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack? 163 | if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis? 164 | $op = '*'; $index--; // it's an implicit multiplication 165 | } 166 | // heart of the algorithm: 167 | 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])) { 168 | $output[] = $stack->pop(); // pop stuff off the stack into the output 169 | } 170 | // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail 171 | $stack->push($op); // finally put OUR operator onto the stack 172 | $index++; 173 | $expecting_op = false; 174 | //=============== 175 | } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis? 176 | while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last ( 177 | if (is_null($o2)) return $this->trigger("unexpected ')'"); 178 | else $output[] = $o2; 179 | } 180 | 181 | $nextOp = $stack->last(2); 182 | 183 | if ($nextOp !== null && preg_match("/^([A-Za-z]\w*)\($/", $nextOp, $matches)) { // did we just close a function? 184 | $fnn = $matches[1]; // get the function name 185 | $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) 186 | $output[] = $stack->pop(); // pop the function and push onto the output 187 | if (in_array($fnn, $this->fb)) { // check the argument count 188 | if($arg_count > 1) 189 | return $this->trigger("too many arguments ($arg_count given, 1 expected)"); 190 | } elseif (array_key_exists($fnn, $this->f)) { 191 | if ($arg_count != count($this->f[$fnn]['args'])) 192 | return $this->trigger("wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)"); 193 | } else { // did we somehow push a non-function on the stack? this should never happen 194 | return $this->trigger("internal error"); 195 | } 196 | } 197 | $index++; 198 | //=============== 199 | } elseif ($op == ',' and $expecting_op) { // did we just finish a function argument? 200 | while (($o2 = $stack->pop()) != '(') { 201 | if (is_null($o2)) return $this->trigger("unexpected ','"); // oops, never had a ( 202 | else $output[] = $o2; // pop the argument expression stuff and push onto the output 203 | } 204 | // make sure there was a function 205 | if (!preg_match("/^([A-Za-z]\w*)\($/", $stack->last(2), $matches)) 206 | return $this->trigger("unexpected ','"); 207 | $stack->push($stack->pop()+1); // increment the argument count 208 | $stack->push('('); // put the ( back on, we'll need to pop back to it again 209 | $index++; 210 | $expecting_op = false; 211 | //=============== 212 | } elseif ($op == '(' and !$expecting_op) { 213 | $stack->push('('); // that was easy 214 | $index++; 215 | $allow_neg = true; 216 | //=============== 217 | } elseif ($ex and !$expecting_op) { // do we now have a function/variable/number? 218 | $expecting_op = true; 219 | $val = $match[1]; 220 | if (preg_match("/^([A-Za-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses... 221 | if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func 222 | $stack->push($val); 223 | $stack->push(1); 224 | $stack->push('('); 225 | $expecting_op = false; 226 | } else { // it's a var w/ implicit multiplication 227 | $val = $matches[1]; 228 | $output[] = $val; 229 | } 230 | } else { // it's a plain old var or num 231 | $output[] = $val; 232 | } 233 | $index += strlen($val); 234 | //=============== 235 | } elseif ($op == ')') { // miscellaneous error checking 236 | return $this->trigger("unexpected ')'"); 237 | } elseif (in_array($op, $ops) and !$expecting_op) { 238 | return $this->trigger("unexpected operator '$op'"); 239 | } else { // I don't even want to know what you did to get here 240 | return $this->trigger("an unexpected error occured"); 241 | } 242 | if ($index == strlen($expr)) { 243 | if (in_array($op, $ops)) { // did we end with an operator? bad. 244 | return $this->trigger("operator '$op' lacks operand"); 245 | } else { 246 | break; 247 | } 248 | } 249 | while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace 250 | $index++; // into implicit multiplication if no operator is there) 251 | } 252 | 253 | } 254 | while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output 255 | if ($op == '(') return $this->trigger("expecting ')'"); // if there are (s on the stack, ()s were unbalanced 256 | $output[] = $op; 257 | } 258 | return $output; 259 | } 260 | 261 | // evaluate postfix notation 262 | public function pfx($tokens, $vars = array()) 263 | { 264 | if ($tokens == false) return false; 265 | 266 | $stack = new Stack; 267 | 268 | foreach ($tokens as $token) { // nice and easy 269 | // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on 270 | if (in_array($token, array('+', '-', '*', '/', '^', '%'))) { 271 | if (is_null($op2 = $stack->pop())) return $this->trigger("internal error"); 272 | if (is_null($op1 = $stack->pop())) return $this->trigger("internal error"); 273 | switch ($token) { 274 | case '+': 275 | $stack->push($op1+$op2); break; 276 | case '-': 277 | $stack->push($op1-$op2); break; 278 | case '*': 279 | $stack->push($op1*$op2); break; 280 | case '/': 281 | if ($op2 == 0) return $this->trigger("division by zero"); 282 | $stack->push($op1/$op2); break; 283 | case '^': 284 | $stack->push(pow($op1, $op2)); break; 285 | case '%': 286 | $stack->push($op1%$op2); break; 287 | } 288 | // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on 289 | } elseif ($token == "_") { 290 | $stack->push(-1*$stack->pop()); 291 | // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on 292 | } elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function! 293 | $fnn = $matches[1]; 294 | if (in_array($fnn, $this->fb)) { // built-in function: 295 | if (is_null($op1 = $stack->pop())) return $this->trigger("internal error"); 296 | $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms 297 | if ($fnn == 'ln') $fnn = 'log'; 298 | eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval() 299 | } elseif (array_key_exists($fnn, $this->f)) { // user function 300 | // get args 301 | $args = array(); 302 | for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) { 303 | if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger("internal error"); 304 | } 305 | $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!! 306 | } 307 | // if the token is a number or variable, push it on the stack 308 | } else { 309 | if (is_numeric($token)) { 310 | $stack->push($token); 311 | } elseif (array_key_exists($token, $this->v)) { 312 | $stack->push($this->v[$token]); 313 | } elseif (array_key_exists($token, $vars)) { 314 | $stack->push($vars[$token]); 315 | } else { 316 | return $this->trigger("undefined variable '$token'"); 317 | } 318 | } 319 | } 320 | // when we're out of tokens, the stack should have a single element, the final result 321 | if ($stack->count != 1) return $this->trigger("internal error"); 322 | return $stack->pop(); 323 | } 324 | 325 | // trigger an error, but nicely, if need be 326 | public function trigger($msg) 327 | { 328 | $this->last_error = $msg; 329 | if (!$this->suppress_errors) 330 | { 331 | echo "\nError found in:"; 332 | $this->debugPrintCallingFunction(); 333 | 334 | trigger_error($msg, E_USER_WARNING); 335 | } 336 | return false; 337 | } 338 | 339 | # Prints the file name, function name, and 340 | # line number which called your function 341 | # (not this function, then one that called 342 | # it to begin with) 343 | public function debugPrintCallingFunction() 344 | { 345 | $file = 'n/a'; 346 | $func = 'n/a'; 347 | $line = 'n/a'; 348 | $debugTrace = debug_backtrace(); 349 | if (isset($debugTrace[1])) { 350 | $file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a'; 351 | $line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a'; 352 | } 353 | if (isset($debugTrace[2])) $func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a'; 354 | echo "\n$file, $func, $line\n"; 355 | } 356 | } 357 | --------------------------------------------------------------------------------