PHP Math Expression Parser

  Page 1 of 1  1
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:
<?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:

code:
<?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_';
?>
  Page 1 of 1  1