April 12, 2009 8:48pm
I created a math expression parser that evaluates math expressions and functions such as "variable = 42 * (34 + 52 - sqrt(variable + 3))".
code:
To use it, include that file and create a new Expression object, then call the evaluate($expr) to evaluate expressions.
An example usage:
code:
<?php
/**
* A math expression parser, supports:
* <ol>
* <li>add, subtract, multiply and division</li>
* <li>brackets grouping</li>
* <li>functions, custom functions' prefix can be customized.</li>
* <li>variables</li>
* </ol>
* Does not support:
* <ol>
* <li>bitwise/logical operators.</li>
* <li>if/else/for statements</li>
* <li>arrays</li>
* </ol>
* Depends on BCMath library to gain arbitrary precision features.<br />
* BNF Grammar (quotes strings and ALL CAPs are terminals):
* <pre>
* expression ::= ( IDENT '=' expression | addexpr )
* addexpr ::= term ( ( '+' | '-' ) term )*
* term ::= primary ( ( '*'|'/' ) primary )*
* primary ::= ( NUMBER | IDENT | '(' expression ')' | IDENT ( '(' expression (',' expression)* ')' | '(' ')' ) )
* </pre>
* This class implements ArrayAccess, so <code>$expr['name']</code> will access variable <code>name</code>.
* To evaluate an expression, use the <code>evaluate(string $expr)</code> method.
* To set percision, use <code>bcscale(int $scale)</code> function.
* Example usage: <code>$expr = new Expression(); $expr->evaluate('a = 12 + 5'); echo $expr['a']; //prints 17</code>
* @author SpaceMan
* @version 1.0
*/
class Expression implements ArrayAccess {
private $tokens = array();
private $expr = '';
/**
* @var array List of all variables.
*/
public $variables = array();
/**
* @var string Prefix of all custom functions, for example, <code>sqrt(10)</code> will map
* to <code>bcsqrt(10)</code> if the prefix is <code>bc</code>.
*/
public $prefix = 'bc';
/**
* Constructs a new Expression object.
* @param string $expr The initial expression to be evaluated.
*/
public function __construct($expr = '') {
$this->expr = $expr;
}
/**
* Evaluates an expression.
* @param string $expr The expression to be evaluated.
* @return string The result in string of numbers.
*/
public function evaluate($expr) {
$this->expr = $expr;
$this->tokenize();
return $this->expression();
}
public function offsetSet($offset, $value) {
$this->variables[$offset] = $value;
}
public function offsetExists($offset) {
return isset($this->variables[$offset]);
}
public function offsetUnset($offset) {
unset($this->variables[$offset]);
}
public function offsetGet($offset) {
return isset($this->variables[$offset]) ? $this->variables[$offset] : null;
}
/**
* Tokenize the expression.
* @internal
*/
private function tokenize() {
$expr = $this->expr;
$i = 0;
$c = " ";
//while there are more string to be tokenized
while ($c) {
//exit if there are no more string.
if ($i >= strlen($expr)) {
return;
}
//the code to be scanned
$c = substr($expr, $i);
if (preg_match('/^\d+(\.\d+)?/', $c, $matches, PREG_OFFSET_CAPTURE)) {
//numbers
$i += strlen($matches[0][0]);
array_push($this->tokens, array('NUMBER', $matches[0][0]));
} else if (preg_match('/^[A-Za-z0-9_]+/', $c, $matches, PREG_OFFSET_CAPTURE)) {
//variables
$i += strlen($matches[0][0]);
array_push($this->tokens, array('IDENT', $matches[0][0]));
} else if ($c[0] == ' ' or $c[0] == '\t' or $c[0] == '\r' or $c[0] == '\n') {
//whitespaces
$i ++;
} else {
//operators
array_push($this->tokens, array($expr[$i], $expr[$i]));
$i ++;
}
}
}
/**
* Determines if the next token exists.
* This function takes multiple arguments.
* @internal
*/
private function has() {
$t = $this->tokens[0];
foreach (func_get_args() as $name) {
if ($t[0] == $name) {
return true;
}
}
return false;
}
/**
* Consume the next token.
*/
private function token() {
return array_shift($this->tokens);
}
/**
* Calls a function.
* @internal
*/
private function call($name, $values = array()) {
return eval('return ' . $this->prefix . $name . '(' . implode(',', $values) . ');');
}
private function primary() {
if ($this->has('NUMBER')) {
//numbers
$t = $this->token();
return $t[1];
} else if ($this->has('-')) {
//negative numbers
$this->token();
return -$this->primary();
} else if ($this->has('(')) {
//brackets
$this->token();
$v = $this->expression();
$this->token();
return $v;
} else if ($this->has('IDENT') && $this->tokens[1][0] == '(') {
//function call
$name = $this->token();
$name = $name[1];
$this->token();
$args = array();
if ($this->has(')')) {
//zero arguments
return $this->call($name);
} else {
//one or more arguments
$args[] = $this->expression();
while ($this->has(',')) {
$this->token();
$args[] = $args[] = $this->expression();
}
$v = $this->call($name, $args);
$this->token();
return $v;
}
} else if ($this->has('IDENT')) {
//get variable
$t = $this->token();
return $this->variables[$t[1]] or '0';
} else {
throw new RuntimeException('Syntax error.');
}
}
private function term() {
$v = $this->primary();
while ($this->has('*', '/')) {
$op = $this->token();
$right = $this->primary();
switch ($op[0]) {
case '*':
$v = bcmul($v, $right);
break;
case '/':
$v = bcdiv($v, $right);
break;
default:
throw new RuntimeException('Invalid operator, expection "*" or "/".');
}
}
return $v;
}
private function addexpr() {
$v = $this->term();
while ($this->has('+', '-')) {
$op = $this->token();
$right = $this->term();
switch ($op[0]) {
case '+':
$v = bcadd($v, $right);
break;
case '-':
$v = bcsub($v, $right);
break;
default:
throw new RuntimeException('Invalid operator, expection "+" or "-".');
}
}
return $v;
}
private function expression() {
if ($this->has('IDENT') and $this->tokens[1][0] == '=') {
$left = $this->token();
$left = $left[1];
$this->token();
$right = $this->expression();
$this->variables[$left] = $right;
return $right;
} else {
return $this->addexpr();
}
}
}
?> To use it, include that file and create a new Expression object, then call the evaluate($expr) to evaluate expressions.
An example usage:
<?php
include ('expression.php');
$expr = new Expression();
$expr['variablename'] = 12; //pre-define a variable inside the object
echo $expr->evaluate($_GET['expression']);
echo 'The variable variablename is: ' . $expr['variablename'];
//to set a prefix for custom functions
$expr->prefix = 'myfuncs_';
?>