From d50b6659270e6ad9e654d44e1eed8cebd8d04f0d Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Mon, 26 Nov 2018 10:06:26 -0500 Subject: [PATCH] Fixed Lexor to correctly generate reverse polish notation (#33) * Updated from NeonXP/MathExecutor * Fixed function in () block issue --- README.md | 7 +- src/NXP/Classes/Lexer.php | 263 +++++++++++++++++++------------------- tests/MathTest.php | 14 ++ 3 files changed, 151 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index f5bf318..c15dccb 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,19 @@ A simple math expressions calculator ## Features: -* Built in support for +, -, *, / and power (^) operators +* Built in support for +, -, *, / and power (^) operators plus () * Support for user defined operators * Support for user defined functions -* Unlimited varable length +* Unlimited varable name lengths * String support, as function parameters or as evaluated by PHP * Exceptions on divide by zero, or treat as zero * Unary Minus +* Pi ($pi) and Euler's number ($e) support to 11 decimal places ## Install via Composer: Stable branch ``` -composer require "nxp/math-executor" "dev-master" +composer require "nxp/math-executor" ``` Dev branch (currently unsupported) diff --git a/src/NXP/Classes/Lexer.php b/src/NXP/Classes/Lexer.php index 82b2c53..36eb43c 100644 --- a/src/NXP/Classes/Lexer.php +++ b/src/NXP/Classes/Lexer.php @@ -1,130 +1,133 @@ - - */ -class Lexer -{ - /** - * @var TokenFactory - */ - private $tokenFactory; - - public function __construct($tokenFactory) - { - $this->tokenFactory = $tokenFactory; - } - - /** - * @param string $input Source string of equation - * @return array Tokens stream - * @throws \NXP\Exception\IncorrectExpressionException - */ - public function stringToTokensStream($input) - { - $matches = []; - preg_match_all($this->tokenFactory->getTokenParserRegex(), $input, $matches); - $tokenFactory = $this->tokenFactory; - $tokensStream = array_map( - function ($token) use ($tokenFactory) { - return $tokenFactory->createToken($token); - }, - $matches[0] - ); - - return $tokensStream; - } - - /** - * @param array $tokensStream Tokens stream - * @return array Array of tokens in revers polish notation - * @throws \NXP\Exception\IncorrectExpressionException - */ - public function buildReversePolishNotation($tokensStream) - { - $output = []; - $stack = []; - - foreach ($tokensStream as $token) { - if ($token instanceof TokenString) { - $output[] = $token; - } - if ($token instanceof TokenNumber) { - $output[] = $token; - } - if ($token instanceof TokenVariable) { - $output[] = $token; - } - if ($token instanceof TokenFunction) { - array_push($stack, $token); - } - if ($token instanceof TokenLeftBracket) { - array_push($stack, $token); - } - if ($token instanceof TokenComma) { - while (($current = array_pop($stack)) && (!$current instanceof TokenLeftBracket)) { - $output[] = $current; - if (empty($stack)) { - throw new IncorrectExpressionException(); - } - } - } - if ($token instanceof TokenRightBracket) { - while (($current = array_pop($stack)) && (!$current instanceof TokenLeftBracket)) { - $output[] = $current; - } - if (!empty($stack) && ($stack[count($stack)-1] instanceof TokenFunction)) { - $output[] = array_pop($stack); - } - } - - if ($token instanceof AbstractOperator) { - while ( - count($stack) > 0 && - ($stack[count($stack)-1] instanceof InterfaceOperator) && - (( - $token->getAssociation() == AbstractOperator::LEFT_ASSOC && - $token->getPriority() <= $stack[count($stack)-1]->getPriority() - ) || ( - $token->getAssociation() == AbstractOperator::RIGHT_ASSOC && - $token->getPriority() < $stack[count($stack)-1]->getPriority() - )) - ) { - $output[] = array_pop($stack); - } - - array_push($stack, $token); - } - } - while (!empty($stack)) { - $token = array_pop($stack); - if ($token instanceof TokenLeftBracket || $token instanceof TokenRightBracket) { - throw new IncorrectBracketsException(); - } - $output[] = $token; - } - - return $output; - } -} + + */ +class Lexer +{ + /** + * @var TokenFactory + */ + private $tokenFactory; + + public function __construct($tokenFactory) + { + $this->tokenFactory = $tokenFactory; + } + + /** + * @param string $input Source string of equation + * @return array Tokens stream + * @throws \NXP\Exception\IncorrectExpressionException + */ + public function stringToTokensStream($input) + { + $matches = []; + preg_match_all($this->tokenFactory->getTokenParserRegex(), $input, $matches); + $tokenFactory = $this->tokenFactory; + $tokensStream = array_map( + function ($token) use ($tokenFactory) { + return $tokenFactory->createToken($token); + }, + $matches[0] + ); + + return $tokensStream; + } + + /** + * @param array $tokensStream Tokens stream + * @return array Array of tokens in revers polish notation + * @throws \NXP\Exception\IncorrectExpressionException + */ + public function buildReversePolishNotation($tokensStream) + { + $output = []; + $stack = []; + + foreach ($tokensStream as $token) { + if ($token instanceof TokenString) { + $output[] = $token; + } + elseif ($token instanceof TokenNumber) { + $output[] = $token; + } + elseif ($token instanceof TokenVariable) { + $output[] = $token; + } + elseif ($token instanceof TokenFunction) { + array_push($stack, $token); + } + elseif ($token instanceof AbstractOperator) { + // While we have something on the stack + while (($count = count($stack)) > 0 + && ( + // If it is a function + ($stack[$count-1] instanceof TokenFunction) + + || + // Or the operator at the top of the operator stack + // has (left associative and equal precedence) + // or has greater precedence + (($stack[$count-1] instanceof InterfaceOperator) && + ( + ($stack[$count-1]->getAssociation() == AbstractOperator::LEFT_ASSOC && + $token->getPriority() == $stack[$count-1]->getPriority()) + || + ($stack[$count-1]->getPriority() > $token->getPriority()) + ) + ) + ) + + // And not a left bracket + && ( ! ($stack[$count-1] instanceof TokenLeftBracket)) ) { + $output[] = array_pop($stack); + } + + array_push($stack, $token); + } + elseif ($token instanceof TokenLeftBracket) { + array_push($stack, $token); + } + elseif ($token instanceof TokenRightBracket) { + while (($current = array_pop($stack)) && ( ! ($current instanceof TokenLeftBracket))) { + $output[] = $current; + } + if (!empty($stack) && ($stack[count($stack)-1] instanceof TokenFunction)) { + $output[] = array_pop($stack); + } + } + } + while (!empty($stack)) { + $token = array_pop($stack); + if ($token instanceof TokenLeftBracket || $token instanceof TokenRightBracket) { + throw new IncorrectBracketsException(); + } + $output[] = $token; + } + + return $output; + } +} diff --git a/tests/MathTest.php b/tests/MathTest.php index c5dd60d..9acdb63 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -69,7 +69,21 @@ class MathTest extends \PHPUnit_Framework_TestCase ['(2 + 2)*-2'], ['(2+-2)*2'], + ['1 + 2 * 3 / (min(1, 5) + 2 + 1)'], + ['1 + 2 * 3 / (min(1, 5) - 2 + 5)'], + ['1 + 2 * 3 / (min(1, 5) * 2 + 1)'], + ['1 + 2 * 3 / (min(1, 5) / 2 + 1)'], + ['1 + 2 * 3 / (min(1, 5) / 2 * 1)'], + ['1 + 2 * 3 / (min(1, 5) / 2 / 1)'], + ['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)'], + ['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'], + ['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)'], + ['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'], + + ['sin(10) * cos(50) / min(10, 20/2)'], + ['sin(10) * cos(50) / min(10, (20/2))'], + ['sin(10) * cos(50) / min(10, (max(10,20)/2))'], ['100500 * 3.5E5'], ['100500 * 3.5E-5'],