From 43f0ff3f28d198fbb4e36346fc5f36fa91cf3e18 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Thu, 25 Oct 2018 11:54:54 -0400 Subject: [PATCH 1/3] Support for better invalid expression detection and divide by zero (#30) * Additional validation for bad expressions (*+ for example) * Removing DivisionByZeroException testing for now Added more unit tests. --- src/NXP/Classes/Calculator.php | 2 +- src/NXP/Classes/Token/TokenDegree.php | 10 +++++ src/NXP/Classes/Token/TokenDivision.php | 12 ++++++ src/NXP/Classes/Token/TokenMinus.php | 10 +++++ src/NXP/Classes/Token/TokenMultiply.php | 10 +++++ src/NXP/Classes/Token/TokenPlus.php | 10 +++++ src/NXP/Exception/DivisionByZeroException.php | 19 +++++++++ tests/MathTest.php | 42 +++++++++++++++++-- 8 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/NXP/Exception/DivisionByZeroException.php diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index bfa7015..253e70c 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -54,7 +54,7 @@ class Calculator } } $result = array_pop($stack); - if (!empty($stack)) { + if ($result === null || ! empty($stack)) { throw new IncorrectExpressionException(); } diff --git a/src/NXP/Classes/Token/TokenDegree.php b/src/NXP/Classes/Token/TokenDegree.php index c31b66e..3eec23d 100644 --- a/src/NXP/Classes/Token/TokenDegree.php +++ b/src/NXP/Classes/Token/TokenDegree.php @@ -10,6 +10,8 @@ namespace NXP\Classes\Token; +use NXP\Exception\IncorrectExpressionException; + /** * @author Alexander Kiryukhin */ @@ -41,12 +43,20 @@ class TokenDegree extends AbstractOperator /** * @param InterfaceToken[] $stack + * * @return TokenNumber + * + * @throws \NXP\Exception\IncorrectExpressionException */ public function execute(&$stack) { $op2 = array_pop($stack); $op1 = array_pop($stack); + + if ($op1 === null || $op2 === null) { + throw new IncorrectExpressionException("Power operator requires two operators"); + } + $result = $op1->getValue() ** $op2->getValue(); return new TokenNumber($result); diff --git a/src/NXP/Classes/Token/TokenDivision.php b/src/NXP/Classes/Token/TokenDivision.php index f1c35ff..5bbc35e 100644 --- a/src/NXP/Classes/Token/TokenDivision.php +++ b/src/NXP/Classes/Token/TokenDivision.php @@ -10,6 +10,9 @@ namespace NXP\Classes\Token; +use NXP\Exception\IncorrectExpressionException; +use NXP\Exception\DivisionByZeroException; + /** * @author Alexander Kiryukhin */ @@ -41,12 +44,21 @@ class TokenDivision extends AbstractOperator /** * @param InterfaceToken[] $stack + * * @return $this + * + * @throws \NXP\Exception\IncorrectExpressionException + * @throws \NXP\Exception\DivisionByZeroException */ public function execute(&$stack) { $op2 = array_pop($stack); $op1 = array_pop($stack); + + if ($op1 === null || $op2 === null) { + throw new IncorrectExpressionException("Division requires two operators"); + } + $result = $op2->getValue() != 0 ? $op1->getValue() / $op2->getValue() : 0; return new TokenNumber($result); diff --git a/src/NXP/Classes/Token/TokenMinus.php b/src/NXP/Classes/Token/TokenMinus.php index 0463d4c..566c950 100644 --- a/src/NXP/Classes/Token/TokenMinus.php +++ b/src/NXP/Classes/Token/TokenMinus.php @@ -10,6 +10,8 @@ namespace NXP\Classes\Token; +use NXP\Exception\IncorrectExpressionException; + /** * @author Alexander Kiryukhin */ @@ -41,12 +43,20 @@ class TokenMinus extends AbstractOperator /** * @param InterfaceToken[] $stack + * * @return $this + * + * @throws \NXP\Exception\IncorrectExpressionException */ public function execute(&$stack) { $op2 = array_pop($stack); $op1 = array_pop($stack); + + if ($op1 === null || $op2 === null) { + throw new IncorrectExpressionException("Subtraction requires two operators"); + } + $result = $op1->getValue() - $op2->getValue(); return new TokenNumber($result); diff --git a/src/NXP/Classes/Token/TokenMultiply.php b/src/NXP/Classes/Token/TokenMultiply.php index e6fd960..8b173b9 100644 --- a/src/NXP/Classes/Token/TokenMultiply.php +++ b/src/NXP/Classes/Token/TokenMultiply.php @@ -10,6 +10,8 @@ namespace NXP\Classes\Token; +use NXP\Exception\IncorrectExpressionException; + /** * @author Alexander Kiryukhin */ @@ -41,12 +43,20 @@ class TokenMultiply extends AbstractOperator /** * @param InterfaceToken[] $stack + * * @return $this + * + * @throws \NXP\Exception\IncorrectExpressionException */ public function execute(&$stack) { $op2 = array_pop($stack); $op1 = array_pop($stack); + + if ($op1 === null || $op2 === null) { + throw new IncorrectExpressionException("Multiplication requires two operators"); + } + $result = $op1->getValue() * $op2->getValue(); return new TokenNumber($result); diff --git a/src/NXP/Classes/Token/TokenPlus.php b/src/NXP/Classes/Token/TokenPlus.php index f9562e7..fe5a1d6 100644 --- a/src/NXP/Classes/Token/TokenPlus.php +++ b/src/NXP/Classes/Token/TokenPlus.php @@ -10,6 +10,8 @@ namespace NXP\Classes\Token; +use NXP\Exception\IncorrectExpressionException; + /** * @author Alexander Kiryukhin */ @@ -41,12 +43,20 @@ class TokenPlus extends AbstractOperator /** * @param InterfaceToken[] $stack + * * @return $this + * + * @throws \NXP\Exception\IncorrectExpressionException */ public function execute(&$stack) { $op2 = array_pop($stack); $op1 = array_pop($stack); + + if ($op1 === null || $op2 === null) { + throw new IncorrectExpressionException("Addition requires two operators"); + } + $result = $op1->getValue() + $op2->getValue(); return new TokenNumber($result); diff --git a/src/NXP/Exception/DivisionByZeroException.php b/src/NXP/Exception/DivisionByZeroException.php new file mode 100644 index 0000000..3a9a978 --- /dev/null +++ b/src/NXP/Exception/DivisionByZeroException.php @@ -0,0 +1,19 @@ + + */ +class DivisionByZeroException extends MathExecutorException +{ +} diff --git a/tests/MathTest.php b/tests/MathTest.php index db6e486..c8b8a03 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -11,7 +11,15 @@ namespace NXP\Tests; -use \NXP\MathExecutor; +use NXP\MathExecutor; +use NXP\Exception\DivisionByZeroException; +use NXP\Exception\IncorrectBracketsException; +use NXP\Exception\IncorrectExpressionException; +use NXP\Exception\MathExecutorException; +use NXP\Exception\UnknownFunctionException; +use NXP\Exception\UnknownOperatorException; +use NXP\Exception\UnknownTokenException; +use NXP\Exception\UnknownVariableException; class MathTest extends \PHPUnit_Framework_TestCase { @@ -27,10 +35,28 @@ class MathTest extends \PHPUnit_Framework_TestCase $this->assertEquals($calculator->execute($expression), $phpResult); } + public function testUnknownFunctionException() + { + $calculator = new MathExecutor(); + $this->expectException(UnknownFunctionException::class); + $calculator->execute('1 * fred("wilma") + 3'); + } + + public function testIncorrectExpressionException() + { + $calculator = new MathExecutor(); + $this->expectException(IncorrectExpressionException::class); + $calculator->execute('1 * + '); + } + public function testZeroDivision() { $calculator = new MathExecutor(); $this->assertEquals($calculator->execute('1 / 0'), 0); + + // future version with allow for optional exceptions on divide by zero + // $this->expectException(DivisionByZeroException::class); + // $calculator->execute('1 / 0'); } public function testExponentiation() @@ -63,15 +89,25 @@ class MathTest extends \PHPUnit_Framework_TestCase ['(5 + 3) * -1'], - ['2+2*2'], + ['2- 2*2'], + ['2-(2*2)'], + ['(2- 2)*2'], + ['2 + 2*2'], + ['2+ 2*2'], ['(2+2)*2'], - ['(2+2)*-2'], + ['(2 + 2)*-2'], ['(2+-2)*2'], ['sin(10) * cos(50) / min(10, 20/2)'], ['100500 * 3.5E5'], ['100500 * 3.5E-5'], + + ['-1 + -2'], + ['-1+-2'], + ['-1- -2'], + ['-1/-2'], + ['-1*-2'], ]; } From d50b6659270e6ad9e654d44e1eed8cebd8d04f0d Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Mon, 26 Nov 2018 10:06:26 -0500 Subject: [PATCH 3/3] 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'],