Я хочу использовать PHP для вычисления простых алгебраических выражений типа 8*(5+1)
, введенных с помощью <input>
обычным пользователем (что означает, нормальная нотация : никаких синтаксических изменений, таких как Multiply(8, Add(5, 1)))
. Кроме того, он должен показывать все шаги, но это не сложно. Проблема, прямо сейчас, вычисляет значение выражений.
Примечание: это то, что я думал до сих пор, что довольно неэффективно, но это решение для проверки. Просто замените строки, где это возможно: в нашем примере распознайте строку 5+1
и замените ее на 6
. Затем снова закрепите, замените (6)
на 6
, снова запустите и замените 8*6
на 48
. Например, код для умножения должен выглядеть следующим образом:
for ($a=1; $a < 1000; $a++) { for ($b=1; $b < 1000; $b++) { string_replace($a . '*' . $b, $a*$b, $string); } }
В зависимости от ваших потребностей я бы предложил изучить алгоритм Shunting Yard Algorithm . Это довольно легко реализовать и работает достаточно хорошо.
Вот пример, который я взбесил некоторое время назад: GIST .
Вот код, скопированный / вставленный в один блок:
Определения выражений:
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; } }
Стек (действительно простая реализация):
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); } }
И, наконец, класс исполнителя:
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; } }
Он работает, сначала маркером ввода (на основе границы слова и токенов). Затем он запускает алгоритм Shunting Yard на нем, чтобы преобразовать вход в стек RPN (Reverse Polish Notation). Тогда это просто вопрос выполнения стека. Вот краткий пример:
$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)
Теперь этот пример значительно сложнее, чем вам может понадобиться. Причина в том, что он также обрабатывает группировку и приоритет оператора. Но это достойный пример работающего алгоритма, который не использует EVAL и поддерживает переменные …
Существует класс Math Parser под названием bcParserPHP, который может представлять интерес.
Кажется довольно простым в использовании и довольно мощным.
Пример кода со своего сайта:
$parser = new MathParser(); $parser->setVariable('X', 5); $parser->setVariable('Y', 2); $parser->setExpression('COS(X)+SIN(Y)/2'); echo $parser->getValue();
К сожалению, это коммерческий продукт; Я не знаю, остановит ли это вас использование этого или нет (предположим, это зависит от цены и ваших потребностей).
Некоммерческая альтернатива может быть такой: http://www.phpclasses.org/package/2695-PHP-Safely-evaluate-mathematics-expressions.html
Обратите внимание, что этот класс использует eval()
внутренне, чего я бы избежал, если это было возможно.
В противном случае написание собственного анализатора языка было бы идеальным решением, но не очень разумным для этого в PHP.
Я бы начал с отсечения ввода чего-либо, что не должно быть в выражении (предполагая, что вы просто хотите разрешить добавление, вычитание, умножение, деление и отсутствие переменных):
$expr = preg_replace('/[^0-9+*\/-]/', '', $expr);
и тогда, как только я уверен, что ничего не остается опасным в пользовательском вводе, просто передайте его через eval (), чтобы оценить выражение:
$result = eval("return $expr;");
Не нужно изобретать велосипед.
Отредактировано для внесения поправок Колинка. Благодаря!