6

I want to use PHP to calculate simple algebraic expressions like, 8*(5+1), entered via an <input> tag by a normal user (which means, normal notation: no syntax changes like Multiply(8, Add(5, 1))). Also, it has to show all steps, but that's not hard. The problem, right now, is calculating the value of the expressions.

Note: this is what I thought so far, which is quite inefficient but it's a provisory solution. Just replace strings where possible: in our example, recognize the string 5+1 and replace it with 6. Then, loop again, replace (6) with 6, loop again, and replace 8*6 with 48. The code for multiplying, for example, should look like this:

for ($a=1; $a < 1000; $a++) {
    for ($b=1; $b < 1000; $b++) {
        string_replace($a . '*' . $b, $a*$b, $string);
    }
}
martincarlin87
  • 10,848
  • 24
  • 98
  • 145
Giulio Muscarello
  • 1,312
  • 2
  • 12
  • 33
  • 2
    First thoughts that come to mind is using a stack to push operations and them writing down the order of operations rules you use to decide what operations are performed first. That will help you translate them into code. – thatidiotguy Oct 02 '12 at 14:54
  • 2
    google "Djikstra shunting yard" algorithm, or take a look at the evalmath library on phpclasses – Mark Baker Oct 02 '12 at 14:55
  • For reference, back ticks on a Mac keyboard are next to the `Z` – BenM Oct 02 '12 at 14:55
  • Use a regex for assertion, then just `eval` it. That's what it's for, even though pros like more convoluted approaches, and yes, newbies should generally shy away. – mario Oct 02 '12 at 14:55
  • possible duplicate of [Preg_replace simple math problem with solution?](http://stackoverflow.com/questions/6867740/preg-replace-simple-math-problem-with-solution) and [Securely evaluate simple maths](http://stackoverflow.com/questions/12115277/securely-evaluate-simple-maths) – mario Oct 02 '12 at 14:55
  • why replace if you can break the input and add it?? – geekman Oct 02 '12 at 14:56
  • @BenM They don't seem to be there. Maybe that's because I'm using an Italian keyboard layout? – Giulio Muscarello Oct 02 '12 at 14:57
  • @mario Can you post an example? – Giulio Muscarello Oct 02 '12 at 14:57
  • Did you check the examples on google? https://www.google.com/search?q=php+calculator – Peon Oct 02 '12 at 14:57
  • If you want to do it right, you'll need to parse the expression, and then (for efficiency) convert that infix expression into a prefix or postfix expression, push it to a stack, then calculate as you pop values and operators. – Major Productions Oct 02 '12 at 14:57
  • @mario - it may be a similar question, but the answer on that question is not recommended. The `e` modifier in preg_replace is deprecated. (there are other ways to achieve similar effects, though) – SDC Oct 02 '12 at 14:58
  • @SDC: It's deprecated for good reasons. This one ain't. And yes, there are better duplicates. I'm doing a surface search as I don't care much, but OP is allowed to google for more. – mario Oct 02 '12 at 14:59
  • I noticed all of you are suggesting me eval(). I know I can simply use it, but I need to solve the expression step by step, which need more code than just one eval(). I mean: how to split the string and find the first operation to do? I guess, something like a regex, but I'm not really sure of what to do. – Giulio Muscarello Oct 02 '12 at 15:00
  • Hmmmm. most of use are recommending __against__ the use of eval – Mark Baker Oct 02 '12 at 15:02
  • Consider this at first: you need to parse strings in BODMAS order. (Google it). – AKS Oct 02 '12 at 15:02
  • @MarkBaker's Djikstra shunting yard suggestion seems to be among the best algorithms to use, as it doesn't actually need complex regex (which, actually, I don't know very well as a n00b) and is quite easy to implement. Waiting for other answers, satisfying bot the conditions (solve step-by-step, normal notation). – Giulio Muscarello Oct 02 '12 at 15:02
  • http://www.phpclasses.org/package/2695-PHP-Safely-evaluate-mathematical-expressions.html to avoid reinventing the wheel, providing a good way of learning how to do this, and a safe parser/evaluator – Mark Baker Oct 02 '12 at 15:04
  • Duplicate: http://stackoverflow.com/questions/5057320/php-function-to-evaluate-string-like-2-1-as-arithmetic-2-1-1 , just found it and the answer is the same as mine and I got down-voted thanx allot – ka_lin Oct 02 '12 at 15:04
  • @KA_lin - just because an answer gets upvoted/accepted by some users, doesn't make it a good answer – Mark Baker Oct 02 '12 at 15:06

3 Answers3

23

Depending on your needs, I would suggest looking into the Shunting Yard Algorithm. It's pretty easy to implement, and works quite well.

Here's an example I whipped up a while ago: GIST.

Here's the code copy/pasted into one block:

Expression Definitions:

class Parenthesis extends TerminalExpression {

    protected $precidence = 7;

    public function operate(Stack $stack) {
    }

    public function getPrecidence() {
        return $this->precidence;
    }

    public function isNoOp() {
        return true;
    }

    public function isParenthesis() {
        return true;
    }

    public function isOpen() {
        return $this->value == '(';
    }

}

class Number extends TerminalExpression {

    public function operate(Stack $stack) {
        return $this->value;
    }

}

abstract class Operator extends TerminalExpression {

    protected $precidence = 0;
    protected $leftAssoc = true;

    public function getPrecidence() {
        return $this->precidence;
    }

    public function isLeftAssoc() {
        return $this->leftAssoc;
    }

    public function isOperator() {
        return true;
    }

}

class Addition extends Operator {

    protected $precidence = 4;

    public function operate(Stack $stack) {
        return $stack->pop()->operate($stack) + $stack->pop()->operate($stack);
    }

}

class Subtraction extends Operator {

    protected $precidence = 4;

    public function operate(Stack $stack) {
        $left = $stack->pop()->operate($stack);
        $right = $stack->pop()->operate($stack);
        return $right - $left;
    }

}

class Multiplication extends Operator {

    protected $precidence = 5;

    public function operate(Stack $stack) {
        return $stack->pop()->operate($stack) * $stack->pop()->operate($stack);
    }

}

class Division extends Operator {

    protected $precidence = 5;

    public function operate(Stack $stack) {
        $left = $stack->pop()->operate($stack);
        $right = $stack->pop()->operate($stack);
        return $right / $left;
    }

}

class Power extends Operator {

    protected $precidence=6;

    public function operate(Stack $stack) {
        $left = $stack->pop()->operate($stack);
        $right = $stack->pop()->operate($stack);
        return pow($right, $left);
    }
}

abstract class TerminalExpression {

    protected $value = '';

    public function __construct($value) {
        $this->value = $value;
    }

    public static function factory($value) {
        if (is_object($value) && $value instanceof TerminalExpression) {
            return $value;
        } elseif (is_numeric($value)) {
            return new Number($value);
        } elseif ($value == '+') {
            return new Addition($value);
        } elseif ($value == '-') {
            return new Subtraction($value);
        } elseif ($value == '*') {
            return new Multiplication($value);
        } elseif ($value == '/') {
            return new Division($value);
        } elseif ($value == '^') {
            return new Power($value);
        } elseif (in_array($value, array('(', ')'))) {
            return new Parenthesis($value);
        }
        throw new Exception('Undefined Value ' . $value);
    }

    abstract public function operate(Stack $stack);

    public function isOperator() {
        return false;
    }

    public function isParenthesis() {
        return false;
    }

    public function isNoOp() {
        return false;
    }

    public function render() {
        return $this->value;
    }
}

The stack (really simple implementation):

class Stack {

    protected $data = array();

    public function push($element) {
        $this->data[] = $element;
    }

    public function poke() {
        return end($this->data);
    }

    public function pop() {
        return array_pop($this->data);
    }

}

And finally, the executor class:

class Math {

    protected $variables = array();

    public function evaluate($string) {
        $stack = $this->parse($string);
        return $this->run($stack);
    }

    public function parse($string) {
        $tokens = $this->tokenize($string);
        $output = new Stack();
        $operators = new Stack();
        foreach ($tokens as $token) {
            $token = $this->extractVariables($token);
            $expression = TerminalExpression::factory($token);
            if ($expression->isOperator()) {
                $this->parseOperator($expression, $output, $operators);
            } elseif ($expression->isParenthesis()) {
                $this->parseParenthesis($expression, $output, $operators);
            } else {
                $output->push($expression);
            }
        }
        while (($op = $operators->pop())) {
            if ($op->isParenthesis()) {
                throw new RuntimeException('Mismatched Parenthesis');
            }
            $output->push($op);
        }
        return $output;
    }

    public function registerVariable($name, $value) {
        $this->variables[$name] = $value;
    }

    public function run(Stack $stack) {
        while (($operator = $stack->pop()) && $operator->isOperator()) {
            $value = $operator->operate($stack);
            if (!is_null($value)) {
                $stack->push(TerminalExpression::factory($value));
            }
        }
        return $operator ? $operator->render() : $this->render($stack);
    }

    protected function extractVariables($token) {
        if ($token[0] == '$') {
            $key = substr($token, 1);
            return isset($this->variables[$key]) ? $this->variables[$key] : 0;
        }
        return $token;
    }

    protected function render(Stack $stack) {
        $output = '';
        while (($el = $stack->pop())) {
            $output .= $el->render();
        }
        if ($output) {
            return $output;
        }
        throw new RuntimeException('Could not render output');
    }

    protected function parseParenthesis(TerminalExpression $expression, Stack $output, Stack $operators) {
        if ($expression->isOpen()) {
            $operators->push($expression);
        } else {
            $clean = false;
            while (($end = $operators->pop())) {
                if ($end->isParenthesis()) {
                    $clean = true;
                    break;
                } else {
                    $output->push($end);
                }
            }
            if (!$clean) {
                throw new RuntimeException('Mismatched Parenthesis');
            }
        }
    }

    protected function parseOperator(TerminalExpression $expression, Stack $output, Stack $operators) {
        $end = $operators->poke();
        if (!$end) {
            $operators->push($expression);
        } elseif ($end->isOperator()) {
            do {
                if ($expression->isLeftAssoc() && $expression->getPrecidence() <= $end->getPrecidence()) {
                    $output->push($operators->pop());
                } elseif (!$expression->isLeftAssoc() && $expression->getPrecidence() < $end->getPrecidence()) {
                    $output->push($operators->pop());
                } else {
                    break;
                }
            } while (($end = $operators->poke()) && $end->isOperator());
            $operators->push($expression);
        } else {
            $operators->push($expression);
        }
    }

    protected function tokenize($string) {
        $parts = preg_split('((\d+|\+|-|\(|\)|\*|/)|\s+)', $string, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        $parts = array_map('trim', $parts);
        return $parts;
    }

}

It works by first tokenizing the input (based on word boundary, and tokens). Then, it runs the Shunting Yard algorithm on it to convert the input into a RPN (Reverse Polish Notation) stack. Then, it's just a matter of executing the stack. Here's a quick example:

$math = new Math();

$answer = $math->evaluate('(2 + 3) * 4');
var_dump($answer);
// int(20)

$answer = $math->evaluate('1 + 2 * ((3 + 4) * 5 + 6)');
var_dump($answer);
// int(83)

$answer = $math->evaluate('(1 + 2) * (3 + 4) * (5 + 6)');
var_dump($answer);
// int(231)

$math->registerVariable('a', 4);
$answer = $math->evaluate('($a + 3) * 4');
var_dump($answer);
// int(28)

$math->registerVariable('a', 5);
$answer = $math->evaluate('($a + $a) * 4');
var_dump($answer);
// int(40)

Now, this example is significantly more complex than you may need. The reason is that it also handles grouping and operator precedence. But it's a decent example of a running algorithm that doesn't use EVAL and supports variables...

Giulio Muscarello
  • 1,312
  • 2
  • 12
  • 33
ircmaxell
  • 163,128
  • 34
  • 264
  • 314
  • It's perfect for my needs, although it seems to give errors working with variables. Exactly: Fatal error: Uncaught exception 'Exception' with message 'Undefined Value a' in /index.php:122 Stack trace: #0 /index.php(177): TerminalExpression::factory('a') #1 /index.php(167): Math->parse('($a + 3) * 4') #2 /index.php(290): Math->evaluate('($a + 3) * 4') #3 {main} thrown in /index.php on line 122 – Giulio Muscarello Oct 02 '12 at 15:48
  • I had problems with line breaks, so i made a few unsuccessful edits. Anyway. It misses one of the key features I need, which is solving step-by-step. I could give a try with explode() using parenthesis, but still it's not the solution. – Giulio Muscarello Oct 02 '12 at 15:51
  • @GiulioMuscarello: I fixed the issues with the tokenizer regex. It'll now handle linebreaks and variables correctly. Just replace the last method of the Math class (as that has the corrected regex)... – ircmaxell Oct 02 '12 at 16:04
  • [This lecture](http://www.youtube.com/watch?v=4F72VULWFvc) should be mentioned as related material. – tereško Oct 02 '12 at 16:29
  • @ircmaxell The code now works perfectly, thanks. Would you like to be cited, or is any attribution required? – Giulio Muscarello Oct 02 '12 at 16:37
  • @GiulioMuscarello: Just put the link to the gist in the comments. Other than that, it's completely up to you... – ircmaxell Oct 02 '12 at 16:45
  • Okay, thanks. FYI [if you'll ever need it, LOL], power (x^n) doesn't work, but I'm trying to fix it myself. – Giulio Muscarello Oct 02 '12 at 16:56
  • Editing, to add power support (just in case somebody needs it). – Giulio Muscarello Oct 02 '12 at 17:07
  • @ircmaxell hi i have one doubt may i ask – user200 Sep 02 '18 at 02:47
4

There is a Math Parser class called bcParserPHP that might be of interest.

Seems fairly simple to use and pretty powerful.

Example code from their site:

$parser = new MathParser();
$parser->setVariable('X', 5);
$parser->setVariable('Y', 2);
$parser->setExpression('COS(X)+SIN(Y)/2');
echo $parser->getValue();

Unfortunately, it's a commercial product; I don't know if that would stop you using it or not (guess it depends on the price and on your needs).

A non-commercial alternative might be this one: http://www.phpclasses.org/package/2695-PHP-Safely-evaluate-mathematical-expressions.html

Note that this class uses eval() internally, which I would avoid doing if possible.

Failing that, writing your own language parser would be the ideal solution, but not really sensible to do that in PHP.

SDC
  • 14,192
  • 2
  • 35
  • 48
  • The use of eval() in evalmath can easily be avoided with a small bit or code reworking to replace it with a whitelisted function set, and the use of call_user_func_array() – Mark Baker Oct 02 '12 at 19:52
1

I'd start by stripping the input of anything which shouldn't be in the expression (assuming you just want to allow add, subtract, multiply, divide, and no variables):

 $expr = preg_replace('/[^0-9+*\/-]/', '', $expr);

and then, once I'm confident nothing dangerous remains in the user input, simply pass the itthrough eval() to evaluate the expression:

 $result = eval("return $expr;");

No need to reinvent the wheel.

Edited to incorporate Kolink's corrections. Thanks!

Aaron Miller
  • 3,692
  • 1
  • 19
  • 26
  • `eval` doesn't return anything unless there's a `return` statement in it - unlike in JavaScript where the return value is the value of the last expression. Also, don't forget to escape the characters you use as a delimiter. – Niet the Dark Absol Oct 02 '12 at 15:02
  • Thanks for the corrections -- I've updated the answer. That'll teach me to answer from my phone while I'm out having a smoke... – Aaron Miller Oct 02 '12 at 15:13
  • 1
    Well, that's not a great thing to do, since if there's a syntax error (for example, if I did `0 + 1 1` as input) it'll bork the entire system... Better to use a full fledged parser... – ircmaxell Oct 02 '12 at 15:40