From e7add2d36dce3a25389e359bd53e4b5ebc6160f9 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Thu, 9 Apr 2020 22:06:58 -0400 Subject: [PATCH 01/14] Update documentation for PHPFUI/InstaDoc --- README.md | 6 ++- src/NXP/Classes/Calculator.php | 6 +-- src/NXP/Classes/Lexer.php | 1 + src/NXP/Classes/Token/AbstractOperator.php | 1 + src/NXP/Classes/TokenFactory.php | 8 ++++ src/NXP/MathExecutor.php | 44 +++++++++++++++------- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 05ff3cc..7d30e78 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,10 @@ This will allow you to remove functions and operators if needed, or implement di Also note that you can replace an existing default operator by adding a new operator with the same regular expression string. For example if you just need to redefine TokenPlus, you can just add a new operator with the same regex string, in this case '\\+'. +## Documentation + +Full class documentation via [PHPFUI/InstaDoc](http://phpfui.com/?n=NXP&c=MathExecutor) + ## Future Enhancements -This package will continue to track currently supported versions of PHP. We recommend you keep PHP up-to-date. Currently the code will run under 5.6, but don't expect 5.6 support going forward. +This package will continue to track currently supported versions of PHP. PHP 7.1 and earlier support will be dropped when PHP 8 is released. diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 980a52b..7d82ecd 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -26,9 +26,9 @@ class Calculator { /** * Calculate array of tokens in reverse polish notation - * @param array $tokens Array of tokens - * @param array $variables Array of variables - * @return number Result + * @param array $tokens + * @param array $variables + * @return number Result * @throws \NXP\Exception\IncorrectExpressionException * @throws \NXP\Exception\UnknownVariableException */ diff --git a/src/NXP/Classes/Lexer.php b/src/NXP/Classes/Lexer.php index cd12d21..a7fe697 100644 --- a/src/NXP/Classes/Lexer.php +++ b/src/NXP/Classes/Lexer.php @@ -155,6 +155,7 @@ class Lexer private function isNegativeZero($x) { $floatVal = floatval($x); + return $floatVal === 0.0 && $floatVal ** -1 === -INF; } } diff --git a/src/NXP/Classes/Token/AbstractOperator.php b/src/NXP/Classes/Token/AbstractOperator.php index 8b23ad8..67e8031 100644 --- a/src/NXP/Classes/Token/AbstractOperator.php +++ b/src/NXP/Classes/Token/AbstractOperator.php @@ -35,6 +35,7 @@ abstract class AbstractOperator implements InterfaceToken, InterfaceOperator public function setDivisionByZeroException($exception = true) { $this->divideByZeroReporting = $exception; + return $this; } diff --git a/src/NXP/Classes/TokenFactory.php b/src/NXP/Classes/TokenFactory.php index 5aa634a..70d1ba9 100644 --- a/src/NXP/Classes/TokenFactory.php +++ b/src/NXP/Classes/TokenFactory.php @@ -54,6 +54,7 @@ class TokenFactory * @param string $name * @param callable $function * @param int $places + * @return TokenFactory * @throws \ReflectionException */ public function addFunction($name, callable $function, $places = null) @@ -63,6 +64,8 @@ class TokenFactory $places = $reflector->getNumberOfParameters(); } $this->functions[$name] = [$places, $function]; + + return $this; } /** @@ -79,6 +82,7 @@ class TokenFactory /** * Add operator * @param string $operatorClass + * @return TokenFactory * @throws UnknownOperatorException * @throws \ReflectionException */ @@ -91,6 +95,8 @@ class TokenFactory } $this->operators[$operatorClass::getRegex()] = $operatorClass; + + return $this; } /** @@ -113,6 +119,7 @@ class TokenFactory public function setDivisionByZeroException($exception = true) { $this->divideByZeroReporting = $exception; + return $this; } @@ -147,6 +154,7 @@ class TokenFactory TokenComma::getRegex() ); $s .= $operatorsRegex . '/i'; + return $s; } diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index 9d3f5e8..e9ce0ed 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -133,6 +133,7 @@ class MathExecutor /** * Remove all variables + * @return MathExecutor */ public function removeVars() { @@ -196,7 +197,6 @@ class MathExecutor * Set division by zero exception reporting * * @param bool $exception default true - * * @return MathExecutor */ public function setDivisionByZeroException($exception = true) @@ -256,25 +256,36 @@ class MathExecutor $this->setVars($this->defaultVars()); } + /** + * Get the default operators + * + * @return array of class names + */ protected function defaultOperators() { return [ - 'NXP\Classes\Token\TokenPlus', - 'NXP\Classes\Token\TokenMinus', - 'NXP\Classes\Token\TokenMultiply', - 'NXP\Classes\Token\TokenDivision', - 'NXP\Classes\Token\TokenDegree', - 'NXP\Classes\Token\TokenAnd', - 'NXP\Classes\Token\TokenOr', - 'NXP\Classes\Token\TokenEqual', - 'NXP\Classes\Token\TokenNotEqual', - 'NXP\Classes\Token\TokenGreaterThanOrEqual', - 'NXP\Classes\Token\TokenGreaterThan', - 'NXP\Classes\Token\TokenLessThanOrEqual', - 'NXP\Classes\Token\TokenLessThan', + \NXP\Classes\Token\TokenPlus::class, + \NXP\Classes\Token\TokenMinus::class, + \NXP\Classes\Token\TokenMultiply::class, + \NXP\Classes\Token\TokenDivision::class, + \NXP\Classes\Token\TokenDegree::class, + \NXP\Classes\Token\TokenAnd::class, + \NXP\Classes\Token\TokenOr::class, + \NXP\Classes\Token\TokenEqual::class, + \NXP\Classes\Token\TokenNotEqual::class, + \NXP\Classes\Token\TokenGreaterThanOrEqual::class, + \NXP\Classes\Token\TokenGreaterThan::class, + \NXP\Classes\Token\TokenLessThanOrEqual::class, + \NXP\Classes\Token\TokenLessThan::class, ]; } + /** + * Gets the default functions as an array. Key is function name + * and value is the function as a closure. + * + * @return array + */ protected function defaultFunctions() { return [ @@ -413,6 +424,11 @@ class MathExecutor ]; } + /** + * Returns the default variables names as key/value pairs + * + * @return array + */ protected function defaultVars() { return [ From cab8e2d38ae1c8c7fb75022f7d9b0539a0a86d4e Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Fri, 15 May 2020 21:51:23 +0300 Subject: [PATCH 02/14] Massive refactoring More clean structure Parsing without regular expressions --- README.md | 84 ++-- composer.json | 51 ++- src/NXP/Classes/Calculator.php | 61 ++- src/NXP/Classes/CustomFunction.php | 63 +++ src/NXP/Classes/Lexer.php | 161 -------- src/NXP/Classes/Operator.php | 69 ++++ src/NXP/Classes/Token.php | 35 ++ .../Classes/Token/AbstractContainerToken.php | 46 --- src/NXP/Classes/Token/AbstractOperator.php | 51 --- src/NXP/Classes/Token/InterfaceFunction.php | 23 -- src/NXP/Classes/Token/InterfaceOperator.php | 33 -- src/NXP/Classes/Token/InterfaceToken.php | 22 - src/NXP/Classes/Token/TokenAnd.php | 53 --- src/NXP/Classes/Token/TokenComma.php | 53 --- src/NXP/Classes/Token/TokenDegree.php | 64 --- src/NXP/Classes/Token/TokenDivision.php | 70 ---- src/NXP/Classes/Token/TokenEqual.php | 53 --- src/NXP/Classes/Token/TokenFunction.php | 42 -- src/NXP/Classes/Token/TokenGreaterThan.php | 53 --- .../Classes/Token/TokenGreaterThanOrEqual.php | 53 --- src/NXP/Classes/Token/TokenLeftBracket.php | 25 -- src/NXP/Classes/Token/TokenLessThan.php | 53 --- .../Classes/Token/TokenLessThanOrEqual.php | 53 --- src/NXP/Classes/Token/TokenMinus.php | 68 --- src/NXP/Classes/Token/TokenMultiply.php | 64 --- src/NXP/Classes/Token/TokenNotEqual.php | 53 --- src/NXP/Classes/Token/TokenNumber.php | 25 -- src/NXP/Classes/Token/TokenOr.php | 53 --- src/NXP/Classes/Token/TokenPlus.php | 64 --- src/NXP/Classes/Token/TokenRightBracket.php | 25 -- .../Classes/Token/TokenStringDoubleQuoted.php | 25 -- .../Classes/Token/TokenStringSingleQuoted.php | 26 -- src/NXP/Classes/Token/TokenVariable.php | 25 -- src/NXP/Classes/TokenFactory.php | 217 ---------- src/NXP/Classes/Tokenizer.php | 313 ++++++++++++++ src/NXP/Exception/UnknownTokenException.php | 19 - src/NXP/MathExecutor.php | 388 ++++++++---------- tests/MathTest.php | 59 +-- 38 files changed, 790 insertions(+), 1905 deletions(-) create mode 100644 src/NXP/Classes/CustomFunction.php delete mode 100644 src/NXP/Classes/Lexer.php create mode 100644 src/NXP/Classes/Operator.php create mode 100644 src/NXP/Classes/Token.php delete mode 100644 src/NXP/Classes/Token/AbstractContainerToken.php delete mode 100644 src/NXP/Classes/Token/AbstractOperator.php delete mode 100644 src/NXP/Classes/Token/InterfaceFunction.php delete mode 100644 src/NXP/Classes/Token/InterfaceOperator.php delete mode 100644 src/NXP/Classes/Token/InterfaceToken.php delete mode 100644 src/NXP/Classes/Token/TokenAnd.php delete mode 100644 src/NXP/Classes/Token/TokenComma.php delete mode 100644 src/NXP/Classes/Token/TokenDegree.php delete mode 100644 src/NXP/Classes/Token/TokenDivision.php delete mode 100644 src/NXP/Classes/Token/TokenEqual.php delete mode 100644 src/NXP/Classes/Token/TokenFunction.php delete mode 100644 src/NXP/Classes/Token/TokenGreaterThan.php delete mode 100644 src/NXP/Classes/Token/TokenGreaterThanOrEqual.php delete mode 100644 src/NXP/Classes/Token/TokenLeftBracket.php delete mode 100644 src/NXP/Classes/Token/TokenLessThan.php delete mode 100644 src/NXP/Classes/Token/TokenLessThanOrEqual.php delete mode 100644 src/NXP/Classes/Token/TokenMinus.php delete mode 100644 src/NXP/Classes/Token/TokenMultiply.php delete mode 100644 src/NXP/Classes/Token/TokenNotEqual.php delete mode 100644 src/NXP/Classes/Token/TokenNumber.php delete mode 100644 src/NXP/Classes/Token/TokenOr.php delete mode 100644 src/NXP/Classes/Token/TokenPlus.php delete mode 100644 src/NXP/Classes/Token/TokenRightBracket.php delete mode 100644 src/NXP/Classes/Token/TokenStringDoubleQuoted.php delete mode 100644 src/NXP/Classes/Token/TokenStringSingleQuoted.php delete mode 100644 src/NXP/Classes/Token/TokenVariable.php delete mode 100644 src/NXP/Classes/TokenFactory.php create mode 100644 src/NXP/Classes/Tokenizer.php delete mode 100644 src/NXP/Exception/UnknownTokenException.php diff --git a/README.md b/README.md index 3ed5a95..498d409 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ composer require nxp/math-executor ## Sample usage: ```php -require 'vendor/autoload.php'; +use NXP\MathExecutor;require 'vendor/autoload.php'; -$executor = new \NXP\MathExecutor(); +$executor = new MathExecutor(); echo $executor->execute('1 + 2 * (2 - (4+10))^2 + sin(10)'); ``` @@ -84,60 +84,19 @@ Default operators: `+ - * / ^` Add custom operator to executor: ```php -addOperator(new Operator( + '%', // Operator sign + false, // Is right associated operator + 170, // Operator priority + function (&$stack) { - return '\%'; + $op2 = array_pop($stack); + $op1 = array_pop($stack); + $result = $op1->getValue() % $op2->getValue(); + + return $result; } - - /** - * Priority of this operator - * @return int - */ - public function getPriority() - { - return 180; - } - - /** - * Associaion of this operator (self::LEFT_ASSOC or self::RIGHT_ASSOC) - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * Execution of this operator - * @param InterfaceToken[] $stack Stack of tokens - * @return TokenNumber Result of execution - */ - public function execute(&$stack) - { - $op2 = array_pop($stack); - $op1 = array_pop($stack); - $result = $op1->getValue() % $op2->getValue(); - - return new TokenNumber($result); - } -} -``` - -And adding to executor: - -```php -$executor->addOperator('MyNamespace\ModulusToken'); +)); ``` ## Logical operators: @@ -171,16 +130,25 @@ $executor->setVars([ echo $executor->execute("$var1 + $var2"); ``` ## Division By Zero Support: -By default, the result of division by zero is zero and no error is generated. You have the option to throw a `\NXP\Exception\DivisionByZeroException` by calling `setDivisionByZeroException`. - +Division by zero throws a `\NXP\Exception\DivisionByZeroException` ```php -$executor->setDivisionByZeroException(); try { echo $executor->execute('1/0'); -} catch (\NXP\Exception\DivisionByZeroException $e) { +} catch (DivisionByZeroException $e) { echo $e->getMessage(); } ``` +If you want another behavior, you should override division operator: + +```php +$executor->addOperator("/", false, 180, function($a, $b) { + if ($b == 0) { + return 0; + } + return $a / $b; +}); +echo $executor->execute('1/0'); +``` ## Unary Minus Operator: Negative numbers are supported via the unary minus operator. Positive numbers are not explicitly supported as unsigned numbers are assumed positive. diff --git a/composer.json b/composer.json index 8a5bd99..e4ace7e 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,36 @@ { - "name": "nxp/math-executor", - "description": "Simple math expressions calculator", - "minimum-stability": "stable", - "keywords": ["math","parser","expression","calculator","formula","mathmatics"], - "homepage": "http://github.com/NeonXP/MathExecutor", - "license": "MIT", - "authors": [ - { - "name": "Alexander 'NeonXP' Kiryukhin", - "email": "frei@neonxp.info" - } - ], - "require": { - "php": ">=7.1" + "name": "nxp/math-executor", + "description": "Simple math expressions calculator", + "minimum-stability": "stable", + "keywords": [ + "math", + "parser", + "expression", + "calculator", + "formula", + "mathmatics" + ], + "homepage": "http://github.com/NeonXP/MathExecutor", + "license": "MIT", + "authors": [ + { + "name": "Alexander 'NeonXP' Kiryukhin", + "email": "a.kiryukhin@mail.ru" }, - "require-dev": { - "phpunit/phpunit": "~7.0" - }, - "autoload": { - "psr-0": {"NXP": "src/"} + { + "name": "Bruce Wells", + "email": "brucekwells@gmail.com" } + ], + "require": { + "php": ">=7.1.*" + }, + "require-dev": { + "phpunit/phpunit": "~7.0" + }, + "autoload": { + "psr-0": { + "NXP": "src/" + } + } } diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 7d82ecd..21f3178 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -13,10 +13,12 @@ namespace NXP\Classes; use NXP\Classes\Token\InterfaceOperator; use NXP\Classes\Token\TokenFunction; use NXP\Classes\Token\TokenNumber; -use NXP\Classes\Token\TokenStringSingleQuoted; use NXP\Classes\Token\TokenStringDoubleQuoted; +use NXP\Classes\Token\TokenStringSingleQuoted; use NXP\Classes\Token\TokenVariable; use NXP\Exception\IncorrectExpressionException; +use NXP\Exception\UnknownFunctionException; +use NXP\Exception\UnknownOperatorException; use NXP\Exception\UnknownVariableException; /** @@ -24,36 +26,65 @@ use NXP\Exception\UnknownVariableException; */ class Calculator { + /** + * @var CustomFunction[] + */ + private $functions; + + /** + * @var Operator[] + */ + private $operators; + + /** + * Calculator constructor. + * @param CustomFunction[] $functions + * @param Operator[] $operators + */ + public function __construct(array $functions, array $operators) + { + $this->functions = $functions; + $this->operators = $operators; + } + /** * Calculate array of tokens in reverse polish notation - * @param array $tokens - * @param array $variables - * @return number Result - * @throws \NXP\Exception\IncorrectExpressionException - * @throws \NXP\Exception\UnknownVariableException + * @param Token[] $tokens + * @param array $variables + * @return mixed + * @throws IncorrectExpressionException + * @throws UnknownVariableException */ public function calculate($tokens, $variables) { + /** @var Token[] $stack */ $stack = []; foreach ($tokens as $token) { - if ($token instanceof TokenNumber || $token instanceof TokenStringDoubleQuoted || $token instanceof TokenStringSingleQuoted) { + if ($token->type === Token::Literal || $token->type === Token::String) { $stack[] = $token; - } else if ($token instanceof TokenVariable) { - $variable = $token->getValue(); + } else if ($token->type === Token::Variable) { + $variable = $token->value; if (!array_key_exists($variable, $variables)) { throw new UnknownVariableException($variable); } $value = $variables[$variable]; - $stack[] = new TokenNumber($value); - } else if ($token instanceof InterfaceOperator || $token instanceof TokenFunction) { - $stack[] = $token->execute($stack); + $stack[] = new Token(Token::Literal, $value); + } else if ($token->type === Token::Function) { + if (!array_key_exists($token->value, $this->functions)) { + throw new UnknownFunctionException($token->value); + } + $stack[] = $this->functions[$token->value]->execute($stack); + } elseif ($token->type === Token::Operator) { + if (!array_key_exists($token->value, $this->operators)) { + throw new UnknownOperatorException($token->value); + } + $stack[] = $this->operators[$token->value]->execute($stack); } } $result = array_pop($stack); - if ($result === null || ! empty($stack)) { + if ($result === null || !empty($stack)) { throw new IncorrectExpressionException(); } - - return $result->getValue(); + return $result->value; } } diff --git a/src/NXP/Classes/CustomFunction.php b/src/NXP/Classes/CustomFunction.php new file mode 100644 index 0000000..944f7d9 --- /dev/null +++ b/src/NXP/Classes/CustomFunction.php @@ -0,0 +1,63 @@ +name = $name; + $this->function = $function; + if ($places === null) { + $reflection = new ReflectionFunction($function); + $this->places = $reflection->getNumberOfParameters(); + } else { + $this->places = $places; + } + } + + public function execute(&$stack) + { + if (count($stack) < $this->places) { + throw new IncorrectExpressionException(); + } + $args = []; + for ($i = 0; $i < $this->places; $i++) { + array_unshift($args, array_pop($stack)->value); + } + + $result = call_user_func_array($this->function, $args); + + return new Token(Token::Literal, $result); + } + + +} \ No newline at end of file diff --git a/src/NXP/Classes/Lexer.php b/src/NXP/Classes/Lexer.php deleted file mode 100644 index 9964de9..0000000 --- a/src/NXP/Classes/Lexer.php +++ /dev/null @@ -1,161 +0,0 @@ - - */ -class Lexer -{ - /** - * @var TokenFactory - */ - private $tokenFactory; - - public function __construct($tokenFactory) - { - $this->tokenFactory = $tokenFactory; - } - - /** - * @param string $input Source string of equation - * @return array Tokens stream - */ - 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 IncorrectBracketsException - */ - public function buildReversePolishNotation($tokensStream) - { - $output = []; - $stack = []; - $lastToken = null; - - foreach ($tokensStream as $token) { - if ($token instanceof TokenStringDoubleQuoted) { - $output[] = $token; - } elseif ($token instanceof TokenStringSingleQuoted) { - $output[] = $token; - } elseif ($token instanceof TokenNumber) { - // if the number starts with a minus sign, it could be a negative number, or it could be an operator grabbed by the greedy regex - // if previous token is an operator or open bracket, then it negative, otherwise remove the minus sign and put a negative operator on the stack - if ($lastToken !== null) { - $value = $token->getValue(); - if (($value < 0 || $this->isNegativeZero($value)) && ! ($lastToken instanceof AbstractOperator || $lastToken instanceof TokenLeftBracket)) { - $token = new TokenNumber(abs($value)); - $output[] = $token; - $output[] = new TokenMinus('-'); - } else { - $output[] = $token; - } - } else { - $output[] = $token; - } - } elseif ($token instanceof TokenVariable) { - $output[] = $token; - } elseif ($token instanceof TokenFunction) { - $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); - } - - // Comma operators do nothing really, don't put them on the stack - if (! ($token instanceof TokenComma)) { - $stack[] = $token; - } - } elseif ($token instanceof TokenLeftBracket) { - $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); - } - } - $lastToken = $token; - } - while (!empty($stack)) { - $token = array_pop($stack); - if ($token instanceof TokenLeftBracket || $token instanceof TokenRightBracket) { - throw new IncorrectBracketsException(); - } - $output[] = $token; - } - - return $output; - } - - /** - * Check if the value is a negative zero - * - * @param int|float $x The value to check - * @return boolean True if negative zero, false otherwise - */ - private function isNegativeZero($x) - { - $floatVal = floatval($x); - - return $floatVal === 0.0 && $floatVal ** -1 === -INF; - } -} diff --git a/src/NXP/Classes/Operator.php b/src/NXP/Classes/Operator.php new file mode 100644 index 0000000..53550a9 --- /dev/null +++ b/src/NXP/Classes/Operator.php @@ -0,0 +1,69 @@ + + */ + public $function; + + /** + * @var int + */ + public $places; + + /** + * Operator constructor. + * @param string $operator + * @param bool $isRightAssoc + * @param int $priority + * @param callable $function + */ + public function __construct(string $operator, bool $isRightAssoc, int $priority, callable $function) + { + $this->operator = $operator; + $this->isRightAssoc = $isRightAssoc; + $this->priority = $priority; + $this->function = $function; + $this->function = $function; + $reflection = new ReflectionFunction($function); + $this->places = $reflection->getNumberOfParameters(); + } + + public function execute(&$stack) + { + if (count($stack) < $this->places) { + throw new IncorrectExpressionException(); + } + $args = []; + for ($i = 0; $i < $this->places; $i++) { + array_unshift($args, array_pop($stack)->value); + } + + $result = call_user_func_array($this->function, $args); + + return new Token(Token::Literal, $result); + } +} \ No newline at end of file diff --git a/src/NXP/Classes/Token.php b/src/NXP/Classes/Token.php new file mode 100644 index 0000000..49bf741 --- /dev/null +++ b/src/NXP/Classes/Token.php @@ -0,0 +1,35 @@ +type = $type; + $this->value = $value; + } +} diff --git a/src/NXP/Classes/Token/AbstractContainerToken.php b/src/NXP/Classes/Token/AbstractContainerToken.php deleted file mode 100644 index be2da5f..0000000 --- a/src/NXP/Classes/Token/AbstractContainerToken.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -abstract class AbstractContainerToken implements InterfaceToken -{ - /** - * @var string - */ - protected $value; - - /** - * @param string $value - */ - public function __construct($value) - { - $this->value = $value; - } - - /** - * @param string $value - */ - public function setValue($value) - { - $this->value = $value; - } - - /** - * @return string - */ - public function getValue() - { - return $this->value; - } -} diff --git a/src/NXP/Classes/Token/AbstractOperator.php b/src/NXP/Classes/Token/AbstractOperator.php deleted file mode 100644 index 67e8031..0000000 --- a/src/NXP/Classes/Token/AbstractOperator.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -abstract class AbstractOperator implements InterfaceToken, InterfaceOperator -{ - const RIGHT_ASSOC = 'RIGHT'; - const LEFT_ASSOC = 'LEFT'; - - /** - * Divide by zero reporting - * - * @var bool - */ - private $divideByZeroReporting = false; - - /** - * Set division by zero exception reporting - * - * @param bool $exception default true - * - * @return $this - */ - public function setDivisionByZeroException($exception = true) - { - $this->divideByZeroReporting = $exception; - - return $this; - } - - /** - * Get division by zero exception status - * - * @return bool - */ - public function getDivisionByZeroException() - { - return $this->divideByZeroReporting; - } -} diff --git a/src/NXP/Classes/Token/InterfaceFunction.php b/src/NXP/Classes/Token/InterfaceFunction.php deleted file mode 100644 index a457d0e..0000000 --- a/src/NXP/Classes/Token/InterfaceFunction.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ -interface InterfaceFunction -{ - /** - * @param array $stack - * @return $this - */ - public function execute(&$stack); -} diff --git a/src/NXP/Classes/Token/InterfaceOperator.php b/src/NXP/Classes/Token/InterfaceOperator.php deleted file mode 100644 index 9e3bae1..0000000 --- a/src/NXP/Classes/Token/InterfaceOperator.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -interface InterfaceOperator -{ - /** - * @return int - */ - public function getPriority(); - - /** - * @return string - */ - public function getAssociation(); - - /** - * @param array $stack - * @return TokenNumber - */ - public function execute(&$stack); -} diff --git a/src/NXP/Classes/Token/InterfaceToken.php b/src/NXP/Classes/Token/InterfaceToken.php deleted file mode 100644 index db07aeb..0000000 --- a/src/NXP/Classes/Token/InterfaceToken.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -interface InterfaceToken -{ - /** - * @return string - */ - public static function getRegex(); -} diff --git a/src/NXP/Classes/Token/TokenAnd.php b/src/NXP/Classes/Token/TokenAnd.php deleted file mode 100644 index dab4497..0000000 --- a/src/NXP/Classes/Token/TokenAnd.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() && $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenComma.php b/src/NXP/Classes/Token/TokenComma.php deleted file mode 100644 index f6fc068..0000000 --- a/src/NXP/Classes/Token/TokenComma.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ -class TokenComma extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\,'; - } - - /** - * Comma operator is lowest priority - * - * @return int - */ - public function getPriority() - { - return 0; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @param array $stack - * @return TokenNumber - */ - public function execute(&$stack) - { - // Comma operators don't do anything, stack has already executed - } - -} diff --git a/src/NXP/Classes/Token/TokenDegree.php b/src/NXP/Classes/Token/TokenDegree.php deleted file mode 100644 index 0d22f91..0000000 --- a/src/NXP/Classes/Token/TokenDegree.php +++ /dev/null @@ -1,64 +0,0 @@ - -*/ -class TokenDegree extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\^'; - } - - /** - * @return int - */ - public function getPriority() - { - return 220; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::RIGHT_ASSOC; - } - - /** - * @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 deleted file mode 100644 index 328833b..0000000 --- a/src/NXP/Classes/Token/TokenDivision.php +++ /dev/null @@ -1,70 +0,0 @@ - -*/ -class TokenDivision extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\/'; - } - - /** - * @return int - */ - public function getPriority() - { - return 180; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @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"); - } - - if ($this->getDivisionByZeroException() && $op2->getValue() == 0) { - throw new DivisionByZeroException(); - } - - $result = $op2->getValue() != 0 ? $op1->getValue() / $op2->getValue() : 0; - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenEqual.php b/src/NXP/Classes/Token/TokenEqual.php deleted file mode 100644 index b0ac31e..0000000 --- a/src/NXP/Classes/Token/TokenEqual.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() == $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenFunction.php b/src/NXP/Classes/Token/TokenFunction.php deleted file mode 100644 index 432f107..0000000 --- a/src/NXP/Classes/Token/TokenFunction.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class TokenFunction extends AbstractContainerToken implements InterfaceFunction -{ - /** - * @return string - */ - public static function getRegex() - { - return '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; - } - - /** - * @param array $stack - * @return $this - */ - public function execute(&$stack) - { - $args = []; - list($places, $function) = $this->value; - for ($i = 0; $i < $places; $i++) { - array_unshift($args, array_pop($stack)->getValue()); - } - - $result = call_user_func_array($function, $args); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenGreaterThan.php b/src/NXP/Classes/Token/TokenGreaterThan.php deleted file mode 100644 index 51a5aca..0000000 --- a/src/NXP/Classes/Token/TokenGreaterThan.php +++ /dev/null @@ -1,53 +0,0 @@ -'; - } - - /** - * @return int - */ - public function getPriority() - { - return 150; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @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("> requires two operators"); - } - - $result = $op1->getValue() > $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenGreaterThanOrEqual.php b/src/NXP/Classes/Token/TokenGreaterThanOrEqual.php deleted file mode 100644 index aa4425f..0000000 --- a/src/NXP/Classes/Token/TokenGreaterThanOrEqual.php +++ /dev/null @@ -1,53 +0,0 @@ -\='; - } - - /** - * @return int - */ - public function getPriority() - { - return 150; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @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(">= requires two operators"); - } - - $result = $op1->getValue() >= $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenLeftBracket.php b/src/NXP/Classes/Token/TokenLeftBracket.php deleted file mode 100644 index 08165d8..0000000 --- a/src/NXP/Classes/Token/TokenLeftBracket.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class TokenLeftBracket implements InterfaceToken -{ - /** - * @return string - */ - public static function getRegex() - { - return '\('; - } -} diff --git a/src/NXP/Classes/Token/TokenLessThan.php b/src/NXP/Classes/Token/TokenLessThan.php deleted file mode 100644 index d289028..0000000 --- a/src/NXP/Classes/Token/TokenLessThan.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() < $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenLessThanOrEqual.php b/src/NXP/Classes/Token/TokenLessThanOrEqual.php deleted file mode 100644 index 5a2fba3..0000000 --- a/src/NXP/Classes/Token/TokenLessThanOrEqual.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() <= $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenMinus.php b/src/NXP/Classes/Token/TokenMinus.php deleted file mode 100644 index b4b04e3..0000000 --- a/src/NXP/Classes/Token/TokenMinus.php +++ /dev/null @@ -1,68 +0,0 @@ - -*/ -class TokenMinus extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\-'; - } - - /** - * @return int - */ - public function getPriority() - { - return 170; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @param InterfaceToken[] $stack - * - * @return $this - * - * @throws \NXP\Exception\IncorrectExpressionException - */ - public function execute(&$stack) - { - $op2 = array_pop($stack); - $op1 = array_pop($stack); - - if ($op2 === null) { - throw new IncorrectExpressionException("Subtraction requires right operator"); - } - - if (!$op1) { - $op1 = new TokenNumber(0); - } - - $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 deleted file mode 100644 index 762fb48..0000000 --- a/src/NXP/Classes/Token/TokenMultiply.php +++ /dev/null @@ -1,64 +0,0 @@ - -*/ -class TokenMultiply extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\*'; - } - - /** - * @return int - */ - public function getPriority() - { - return 180; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @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/TokenNotEqual.php b/src/NXP/Classes/Token/TokenNotEqual.php deleted file mode 100644 index 21c9454..0000000 --- a/src/NXP/Classes/Token/TokenNotEqual.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() != $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenNumber.php b/src/NXP/Classes/Token/TokenNumber.php deleted file mode 100644 index 982e316..0000000 --- a/src/NXP/Classes/Token/TokenNumber.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class TokenNumber extends AbstractContainerToken -{ - /** - * @return string - */ - public static function getRegex() - { - return '\-?\d+\.?\d*(E-?\d+)?'; - } -} diff --git a/src/NXP/Classes/Token/TokenOr.php b/src/NXP/Classes/Token/TokenOr.php deleted file mode 100644 index 86a4e53..0000000 --- a/src/NXP/Classes/Token/TokenOr.php +++ /dev/null @@ -1,53 +0,0 @@ -getValue() || $op2->getValue(); - - return new TokenNumber($result); - } -} diff --git a/src/NXP/Classes/Token/TokenPlus.php b/src/NXP/Classes/Token/TokenPlus.php deleted file mode 100644 index 494f786..0000000 --- a/src/NXP/Classes/Token/TokenPlus.php +++ /dev/null @@ -1,64 +0,0 @@ - -*/ -class TokenPlus extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\+'; - } - - /** - * @return int - */ - public function getPriority() - { - return 170; - } - - /** - * @return string - */ - public function getAssociation() - { - return self::LEFT_ASSOC; - } - - /** - * @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/Classes/Token/TokenRightBracket.php b/src/NXP/Classes/Token/TokenRightBracket.php deleted file mode 100644 index 306ae0b..0000000 --- a/src/NXP/Classes/Token/TokenRightBracket.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class TokenRightBracket implements InterfaceToken -{ - /** - * @return string - */ - public static function getRegex() - { - return '\)'; - } -} diff --git a/src/NXP/Classes/Token/TokenStringDoubleQuoted.php b/src/NXP/Classes/Token/TokenStringDoubleQuoted.php deleted file mode 100644 index 6cff262..0000000 --- a/src/NXP/Classes/Token/TokenStringDoubleQuoted.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class TokenStringDoubleQuoted extends AbstractContainerToken -{ - /** - * @return string - */ - public static function getRegex() - { - return '"([^"]|"")*"'; - } -} diff --git a/src/NXP/Classes/Token/TokenStringSingleQuoted.php b/src/NXP/Classes/Token/TokenStringSingleQuoted.php deleted file mode 100644 index 7a7ab92..0000000 --- a/src/NXP/Classes/Token/TokenStringSingleQuoted.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @author Alexander Kiryukhin - */ -class TokenStringSingleQuoted extends AbstractContainerToken -{ - /** - * @return string - */ - public static function getRegex() - { - return "'([^']|'')*'"; - } -} diff --git a/src/NXP/Classes/Token/TokenVariable.php b/src/NXP/Classes/Token/TokenVariable.php deleted file mode 100644 index a4a820a..0000000 --- a/src/NXP/Classes/Token/TokenVariable.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -class TokenVariable extends AbstractContainerToken -{ - /** - * @return string - */ - public static function getRegex() - { - return '\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; - } -} diff --git a/src/NXP/Classes/TokenFactory.php b/src/NXP/Classes/TokenFactory.php deleted file mode 100644 index 70d1ba9..0000000 --- a/src/NXP/Classes/TokenFactory.php +++ /dev/null @@ -1,217 +0,0 @@ - - */ -class TokenFactory -{ - /** - * Available operators - * - * @var array - */ - protected $operators = []; - - /** - * Divide by zero reporting - * - * @var bool - */ - protected $divideByZeroReporting = false; - - /** - * Available functions - * - * @var array - */ - protected $functions = []; - - /** - * Add function - * @param string $name - * @param callable $function - * @param int $places - * @return TokenFactory - * @throws \ReflectionException - */ - public function addFunction($name, callable $function, $places = null) - { - if ($places === null) { - $reflector = new \ReflectionFunction($function); - $places = $reflector->getNumberOfParameters(); - } - $this->functions[$name] = [$places, $function]; - - return $this; - } - - /** - * get functions - * - * @return array containing callback and places indexed by - * function name - */ - public function getFunctions() - { - return $this->functions; - } - - /** - * Add operator - * @param string $operatorClass - * @return TokenFactory - * @throws UnknownOperatorException - * @throws \ReflectionException - */ - public function addOperator($operatorClass) - { - $class = new \ReflectionClass($operatorClass); - - if (!in_array('NXP\Classes\Token\InterfaceToken', $class->getInterfaceNames())) { - throw new UnknownOperatorException($operatorClass); - } - - $this->operators[$operatorClass::getRegex()] = $operatorClass; - - return $this; - } - - /** - * Get registered operators - * - * @return array of operator class names - */ - public function getOperators() - { - return $this->operators; - } - - /** - * Set division by zero exception reporting - * - * @param bool $exception default true - * - * @return TokenFactory - */ - public function setDivisionByZeroException($exception = true) - { - $this->divideByZeroReporting = $exception; - - return $this; - } - - /** - * Get division by zero exception status - * - * @return bool - */ - public function getDivisionByZeroException() - { - return $this->divideByZeroReporting; - } - - /** - * @return string - */ - public function getTokenParserRegex() - { - $operatorsRegex = ''; - foreach ($this->operators as $operator) { - $operatorsRegex .= '|(' . $operator::getRegex() . ')'; - } - $s = sprintf( - '/(%s)|(%s)|(%s)|(%s)|(%s)|([%s%s%s])', - TokenNumber::getRegex(), - TokenStringDoubleQuoted::getRegex(), - TokenStringSingleQuoted::getRegex(), - TokenFunction::getRegex(), - TokenVariable::getRegex(), - TokenLeftBracket::getRegex(), - TokenRightBracket::getRegex(), - TokenComma::getRegex() - ); - $s .= $operatorsRegex . '/i'; - - return $s; - } - - /** - * @param string $token - * @return InterfaceToken - * @throws UnknownTokenException - * @throws UnknownFunctionException - */ - public function createToken($token) - { - if (is_numeric($token)) { - return new TokenNumber($token); - } - - if ($token == '(') { - return new TokenLeftBracket(); - } - - if ($token == ')') { - return new TokenRightBracket(); - } - - if ($token[0] == '"') { - return new TokenStringDoubleQuoted(str_replace('"', '', $token)); - } - - if ($token[0] == "'") { - return new TokenStringSingleQuoted(str_replace("'", '', $token)); - } - - if ($token == ',') { - return new TokenComma(); - } - - foreach ($this->operators as $operator) { - $regex = sprintf('/%s/i', $operator::getRegex()); - if (preg_match($regex, $token)) { - $op = new $operator; - return $op->setDivisionByZeroException($this->getDivisionByZeroException()); - } - } - - $regex = sprintf('/%s/i', TokenVariable::getRegex()); - if (preg_match($regex, $token)) { - return new TokenVariable(substr($token, 1)); - } - - $regex = sprintf('/%s/i', TokenFunction::getRegex()); - if (preg_match($regex, $token)) { - if (isset($this->functions[$token])) { - return new TokenFunction($this->functions[$token]); - } else { - throw new UnknownFunctionException($token); - } - } - - throw new UnknownTokenException($token); - } -} diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php new file mode 100644 index 0000000..869c720 --- /dev/null +++ b/src/NXP/Classes/Tokenizer.php @@ -0,0 +1,313 @@ + + */ +class Tokenizer +{ + /** + * @var Token[] + */ + public $tokens = []; + /** + * @var string + */ + private $input = ""; + /** + * @var string + */ + private $numberBuffer = ""; + /** + * @var string + */ + private $stringBuffer = ""; + /** + * @var bool + */ + private $allowNegative = true; + /** + * @var Operator[] + */ + private $operators = []; + + /** + * @var bool + */ + private $inSingleQuotedString = false; + + /** + * @var bool + */ + private $inDoubleQuotedString = false; + + /** + * Tokenizer constructor. + * @param string $input + * @param Operator[] $operators + */ + public function __construct(string $input, array $operators) + { + $this->input = $input; + $this->operators = $operators; + } + + public function tokenize() + { + foreach (mb_str_split($this->input, 1) as $ch) { + switch (true) { + case $this->inSingleQuotedString: + if ($ch === "'") { + $this->tokens[] = new Token(Token::String, $this->stringBuffer); + $this->inSingleQuotedString = false; + $this->stringBuffer = ""; + continue 2; + } + $this->stringBuffer .= $ch; + continue 2; + case $this->inDoubleQuotedString: + if ($ch === "\"") { + $this->tokens[] = new Token(Token::String, $this->stringBuffer); + $this->inDoubleQuotedString = false; + $this->stringBuffer = ""; + continue 2; + } + $this->stringBuffer .= $ch; + continue 2; + case $ch == " " || $ch == "\n" || $ch == "\r" || $ch == "\t": + $this->tokens[] = new Token(Token::Space, ""); + continue 2; + case $this->isNumber($ch): + if ($this->stringBuffer != "") { + $this->stringBuffer .= $ch; + continue 2; + } + $this->numberBuffer .= $ch; + $this->allowNegative = false; + break; + case strtolower($ch) === "e": + if ($this->numberBuffer != "" && strpos($this->numberBuffer, ".") !== false) { + $this->numberBuffer .= "e"; + $this->allowNegative = true; + break; + } + case $this->isAlpha($ch): + if ($this->numberBuffer != "") { + $this->emptyNumberBufferAsLiteral(); + $this->tokens[] = new Token(Token::Operator, "*"); + } + $this->allowNegative = false; + $this->stringBuffer .= $ch; + break; + case $ch == "\"": + $this->inDoubleQuotedString = true; + continue 2; + case $ch == "'": + $this->inSingleQuotedString = true; + continue 2; + + case $this->isDot($ch): + $this->numberBuffer .= $ch; + $this->allowNegative = false; + break; + case $this->isLP($ch): + if ($this->stringBuffer != "") { + $this->tokens[] = new Token(Token::Function, $this->stringBuffer); + $this->stringBuffer = ""; + } elseif ($this->numberBuffer != "") { + $this->emptyNumberBufferAsLiteral(); + $this->tokens[] = new Token(Token::Operator, "*"); + } + $this->allowNegative = true; + $this->tokens[] = new Token(Token::LeftParenthesis, ""); + break; + case $this->isRP($ch): + $this->emptyNumberBufferAsLiteral(); + $this->emptyStrBufferAsVariable(); + $this->allowNegative = false; + $this->tokens[] = new Token(Token::RightParenthesis, ""); + break; + case $this->isComma($ch): + $this->emptyNumberBufferAsLiteral(); + $this->emptyStrBufferAsVariable(); + $this->allowNegative = true; + $this->tokens[] = new Token(Token::ParamSeparator, ""); + break; + default: + if ($this->allowNegative && $ch == "-") { + $this->allowNegative = false; + $this->numberBuffer .= "-"; + continue 2; + } + $this->emptyNumberBufferAsLiteral(); + $this->emptyStrBufferAsVariable(); + if (count($this->tokens) > 0) { + if ($this->tokens[count($this->tokens) - 1]->type === Token::Operator) { + $this->tokens[count($this->tokens) - 1]->value .= $ch; + } else { + $this->tokens[] = new Token(Token::Operator, $ch); + } + } else { + $this->tokens[] = new Token(Token::Operator, $ch); + } + $this->allowNegative = true; + } + } + $this->emptyNumberBufferAsLiteral(); + $this->emptyStrBufferAsVariable(); + return $this; + } + + private function isNumber($ch) + { + return $ch >= '0' && $ch <= '9'; + } + + private function isAlpha($ch) + { + return $ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || $ch == '_'; + } + + private function emptyNumberBufferAsLiteral() + { + if ($this->numberBuffer != "") { + $this->tokens[] = new Token(Token::Literal, $this->numberBuffer); + $this->numberBuffer = ""; + } + } + + private function isDot($ch) + { + return $ch == '.'; + } + + private function isLP($ch) + { + return $ch == '('; + } + + private function isRP($ch) + { + return $ch == ')'; + } + + private function emptyStrBufferAsVariable() + { + if ($this->stringBuffer != "") { + $this->tokens[] = new Token(Token::Variable, $this->stringBuffer); + $this->stringBuffer = ""; + } + } + + private function isComma($ch) + { + return $ch == ','; + } + + /** + * @return Token[] Array of tokens in revers polish notation + * @throws IncorrectBracketsException + * @throws UnknownOperatorException + */ + public function buildReversePolishNotation() + { + $tokens = []; + /** @var SplStack $stack */ + $stack = new SplStack(); + foreach ($this->tokens as $token) { + switch ($token->type) { + case Token::Literal: + case Token::Variable: + case Token::String: + $tokens[] = $token; + break; + case Token::Function: + case Token::LeftParenthesis: + $stack->push($token); + break; + case Token::ParamSeparator: + while ($stack->top()->type !== Token::LeftParenthesis) { + if ($stack->count() === 0) { + throw new IncorrectBracketsException(); + } + $tokens[] = $stack->pop(); + } + break; + case Token::Operator: + if (!array_key_exists($token->value, $this->operators)) { + throw new UnknownOperatorException(); + } + $op1 = $this->operators[$token->value]; + while ($stack->count() > 0 && $stack->top()->type === Token::Operator) { + if (!array_key_exists($stack->top()->value, $this->operators)) { + throw new UnknownOperatorException(); + } + $op2 = $this->operators[$stack->top()->value]; + if ($op2->priority >= $op1->priority) { + $tokens[] = $stack->pop(); + continue; + } + break; + } + $stack->push($token); + break; + case Token::RightParenthesis: + while (true) { + try { + $ctoken = $stack->pop(); + if ($ctoken->type === Token::LeftParenthesis) { + break; + } + $tokens[] = $ctoken; + } catch (RuntimeException $e) { + throw new IncorrectBracketsException(); + } + } + if ($stack->count() > 0 && $stack->top()->type == Token::Function) { + $tokens[] = $stack->pop(); + } + break; + case Token::Space: + //do nothing + } + } + while ($stack->count() !== 0) { + if ($stack->top()->type === Token::LeftParenthesis || $stack->top()->type === Token::RightParenthesis) { + throw new IncorrectBracketsException(); + } + if ($stack->top()->type === Token::Space) { + $stack->pop(); + continue; + } + $tokens[] = $stack->pop(); + } + return $tokens; + } +} + diff --git a/src/NXP/Exception/UnknownTokenException.php b/src/NXP/Exception/UnknownTokenException.php deleted file mode 100644 index bba28a4..0000000 --- a/src/NXP/Exception/UnknownTokenException.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -class UnknownTokenException extends MathExecutorException -{ -} diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index e9ce0ed..8d3e890 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -12,9 +12,13 @@ namespace NXP; use NXP\Classes\Calculator; -use NXP\Classes\Lexer; +use NXP\Classes\CustomFunction; +use NXP\Classes\Operator; +use NXP\Classes\Token\AbstractOperator; use NXP\Classes\TokenFactory; -use NXP\Exception\UnknownVariableException; +use NXP\Classes\Tokenizer; +use NXP\Exception\DivisionByZeroException; +use ReflectionException; /** * Class MathExecutor @@ -27,12 +31,17 @@ class MathExecutor * * @var array */ - private $variables = []; + public $variables = []; /** - * @var TokenFactory + * @var Operator[] */ - private $tokenFactory; + public $operators = []; + + /** + * @var CustomFunction[] + */ + public $functions = []; /** * @var array @@ -47,213 +56,20 @@ class MathExecutor $this->addDefaults(); } - public function __clone() - { - $this->addDefaults(); - } - - /** - * Get all vars - * - * @return array - */ - public function getVars() - { - return $this->variables; - } - - /** - * Get a specific var - * - * @param string $variable - * @return integer|float - * @throws UnknownVariableException - */ - public function getVar($variable) - { - if (!isset($this->variables[$variable])) { - throw new UnknownVariableException("Variable ({$variable}) not set"); - } - - return $this->variables[$variable]; - } - - /** - * Add variable to executor - * - * @param string $variable - * @param integer|float $value - * @return MathExecutor - * @throws \Exception - */ - public function setVar($variable, $value) - { - if (!is_numeric($value)) { - throw new \Exception("Variable ({$variable}) value must be a number ({$value}) type ({gettype($value)})"); - } - - $this->variables[$variable] = $value; - - return $this; - } - - /** - * Add variables to executor - * - * @param array $variables - * @param bool $clear Clear previous variables - * @return MathExecutor - * @throws \Exception - */ - public function setVars(array $variables, $clear = true) - { - if ($clear) { - $this->removeVars(); - } - - foreach ($variables as $name => $value) { - $this->setVar($name, $value); - } - - return $this; - } - - /** - * Remove variable from executor - * - * @param string $variable - * @return MathExecutor - */ - public function removeVar($variable) - { - unset ($this->variables[$variable]); - - return $this; - } - - /** - * Remove all variables - * @return MathExecutor - */ - public function removeVars() - { - $this->variables = []; - - return $this; - } - - /** - * Add operator to executor - * - * @param string $operatorClass Class of operator token - * @return MathExecutor - * @throws Exception\UnknownOperatorException - */ - public function addOperator($operatorClass) - { - $this->tokenFactory->addOperator($operatorClass); - - return $this; - } - - /** - * Get all registered operators to executor - * - * @return array of operator class names - */ - public function getOperators() - { - return $this->tokenFactory->getOperators(); - } - - /** - * Add function to executor - * - * @param string $name Name of function - * @param callable $function Function - * @param int $places Count of arguments - * @return MathExecutor - * @throws \ReflectionException - */ - public function addFunction($name, $function = null, $places = null) - { - $this->tokenFactory->addFunction($name, $function, $places); - - return $this; - } - - /** - * Get all registered functions - * - * @return array containing callback and places indexed by - * function name - */ - public function getFunctions() - { - return $this->tokenFactory->getFunctions(); - } - - /** - * Set division by zero exception reporting - * - * @param bool $exception default true - * @return MathExecutor - */ - public function setDivisionByZeroException($exception = true) - { - $this->tokenFactory->setDivisionByZeroException($exception); - return $this; - } - - /** - * Get division by zero exception status - * - * @return bool - */ - public function getDivisionByZeroException() - { - return $this->tokenFactory->getDivisionByZeroException(); - } - - /** - * Execute expression - * - * @param $expression - * @return number - */ - public function execute($expression) - { - $cachekey = (string)$expression; - if (!array_key_exists($cachekey, $this->cache)) { - $lexer = new Lexer($this->tokenFactory); - $tokensStream = $lexer->stringToTokensStream($expression); - $tokens = $lexer->buildReversePolishNotation($tokensStream); - $this->cache[$cachekey] = $tokens; - } else { - $tokens = $this->cache[$cachekey]; - } - $calculator = new Calculator(); - $result = $calculator->calculate($tokens, $this->variables); - - return $result; - } - /** * Set default operands and functions + * @throws ReflectionException */ protected function addDefaults() { - $this->tokenFactory = new TokenFactory(); - - foreach ($this->defaultOperators() as $operatorClass) { - $this->tokenFactory->addOperator($operatorClass); + foreach ($this->defaultOperators() as $name => $operator) { + list($callable, $priority, $isRightAssoc) = $operator; + $this->addOperator(new Operator($name, $isRightAssoc, $priority, $callable)); } - foreach ($this->defaultFunctions() as $name => $callable) { - $this->tokenFactory->addFunction($name, $callable); + $this->addFunction($name, $callable); } - - $this->setVars($this->defaultVars()); + $this->variables = $this->defaultVars(); } /** @@ -264,22 +80,115 @@ class MathExecutor protected function defaultOperators() { return [ - \NXP\Classes\Token\TokenPlus::class, - \NXP\Classes\Token\TokenMinus::class, - \NXP\Classes\Token\TokenMultiply::class, - \NXP\Classes\Token\TokenDivision::class, - \NXP\Classes\Token\TokenDegree::class, - \NXP\Classes\Token\TokenAnd::class, - \NXP\Classes\Token\TokenOr::class, - \NXP\Classes\Token\TokenEqual::class, - \NXP\Classes\Token\TokenNotEqual::class, - \NXP\Classes\Token\TokenGreaterThanOrEqual::class, - \NXP\Classes\Token\TokenGreaterThan::class, - \NXP\Classes\Token\TokenLessThanOrEqual::class, - \NXP\Classes\Token\TokenLessThan::class, + '+' => [ + function ($a, $b) { + return $a + $b; + }, + 170, + false + ], + '-' => [ + function ($a, $b) { + return $a - $b; + }, + 170, + false + ], + '*' => [ + function ($a, $b) { + return $a * $b; + }, + 180, + false + ], + '/' => [ + function ($a, $b) { + if ($b == 0) { + throw new DivisionByZeroException(); + } + return $a / $b; + }, + 180, + false + ], + '^' => [ + function ($a, $b) { + return pow($a, $b); + }, + 220, + true + ], + '&&' => [ + function ($a, $b) { + return $a && $b; + }, + 100, + false + ], + '||' => [ + function ($a, $b) { + return $a || $b; + }, + 90, + false + ], + '==' => [ + function ($a, $b) { + return $a == $b; + }, + 140, + false + ], + '!=' => [ + function ($a, $b) { + return $a != $b; + }, + 140, + false + ], + '>=' => [ + function ($a, $b) { + return $a >= $b; + }, + 150, + false + ], + '>' => [ + function ($a, $b) { + return $a > $b; + }, + 150, + false + ], + '<=' => [ + function ($a, $b) { + return $a <= $b; + }, + 150, + false + ], + '<' => [ + function ($a, $b) { + return $a < $b; + }, + 150, + false + ], ]; } + /** + * Add operator to executor + * + * @param Operator $operator + * @return MathExecutor + */ + public function addOperator(Operator $operator) + { + $this->operators[$operator->operator] = $operator; + return $this; + } + /** * Gets the default functions as an array. Key is function name * and value is the function as a closure. @@ -424,6 +333,44 @@ class MathExecutor ]; } + /** + * Execute expression + * + * @param $expression + * @return number + * @throws Exception\IncorrectExpressionException + * @throws Exception\IncorrectBracketsException + * @throws Exception\UnknownOperatorException + * @throws Exception\UnknownVariableException + */ + public function execute($expression) + { + $cachekey = (string)$expression; + if (!array_key_exists($cachekey, $this->cache)) { + $tokens = (new Tokenizer($expression, $this->operators))->tokenize()->buildReversePolishNotation(); + $this->cache[$cachekey] = $tokens; + } else { + $tokens = $this->cache[$cachekey]; + } + $calculator = new Calculator($this->functions, $this->operators); + return $calculator->calculate($tokens, $this->variables); + } + + /** + * Add function to executor + * + * @param string $name Name of function + * @param callable $function Function + * @param int $places Count of arguments + * @return MathExecutor + * @throws ReflectionException + */ + public function addFunction($name, $function = null, $places = null) + { + $this->functions[$name] = new CustomFunction($name, $function, $places); + return $this; + } + /** * Returns the default variables names as key/value pairs * @@ -433,7 +380,12 @@ class MathExecutor { return [ 'pi' => 3.14159265359, - 'e' => 2.71828182846 + 'e' => 2.71828182846 ]; } + + public function __clone() + { + $this->addDefaults(); + } } diff --git a/tests/MathTest.php b/tests/MathTest.php index 7cbc7de..958f14c 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -11,17 +11,15 @@ namespace NXP\Tests; -use NXP\MathExecutor; +use Exception; +use NXP\Classes\Operator; 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; +use NXP\MathExecutor; +use PHPUnit\Framework\TestCase; -class MathTest extends \PHPUnit\Framework\TestCase +class MathTest extends TestCase { /** * @dataProvider providerExpressions @@ -32,15 +30,20 @@ class MathTest extends \PHPUnit\Framework\TestCase /** @var float $phpResult */ eval('$phpResult = ' . $expression . ';'); - $this->assertEquals($phpResult, $calculator->execute($expression), "Expression was: ${expression}"); + try { + $result = $calculator->execute($expression); + } catch (Exception $e) { + $this->fail(sprintf("Exception: %s (%s:%d), expression was: %s", get_class($e), $e->getFile(), $e->getLine(), $expression)); + } + $this->assertEquals($phpResult, $result, "Expression was: ${expression}"); } /** * Expressions data provider - * - * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval. - * The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing - * something more complete and not a simple mathmatical expression. + * + * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval. + * The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing + * something more complete and not a simple mathmatical expression. */ public function providerExpressions() { @@ -223,13 +226,18 @@ class MathTest extends \PHPUnit\Framework\TestCase public function testZeroDivision() { $calculator = new MathExecutor(); + $calculator->addOperator(new Operator("/", false, 180, function ($a, $b) { + if ($b == 0) { + return 0; + } + return $a / $b; + })); $this->assertEquals(0, $calculator->execute('10 / 0')); } public function testZeroDivisionException() { $calculator = new MathExecutor(); - $calculator->setDivisionByZeroException(); $this->expectException(DivisionByZeroException::class); $calculator->execute('10 / 0'); } @@ -244,7 +252,9 @@ class MathTest extends \PHPUnit\Framework\TestCase { $calculator = new MathExecutor(); - $calculator->addFunction('concat', function ($arg1, $arg2) {return $arg1.$arg2;}); + $calculator->addFunction('concat', function ($arg1, $arg2) { + return $arg1 . $arg2; + }); $this->assertEquals('testing', $calculator->execute('concat("test","ing")')); $this->assertEquals('testing', $calculator->execute("concat('test','ing')")); } @@ -252,8 +262,10 @@ class MathTest extends \PHPUnit\Framework\TestCase public function testFunction() { $calculator = new MathExecutor(); - $calculator->addFunction('round', function ($arg) {return round($arg);}); - $this->assertEquals(round(100/30), $calculator->execute('round(100/30)')); + $calculator->addFunction('round', function ($arg) { + return round($arg); + }); + $this->assertEquals(round(100 / 30), $calculator->execute('round(100/30)')); } public function testFunctionIf() @@ -280,12 +292,12 @@ class MathTest extends \PHPUnit\Framework\TestCase public function testEvaluateFunctionParameters() { $calculator = new MathExecutor(); - $calculator->addFunction('round', function ($value, $decimals) - { + $calculator->addFunction('round', function ($value, $decimals) { return round($value, $decimals); - } + } ); $expression = 'round(100 * 1.111111, 2)'; + $phpResult = 0; eval('$phpResult = ' . $expression . ';'); $this->assertEquals($phpResult, $calculator->execute($expression)); $expression = 'round((100*0.04)+(((100*1.02)+0.5)*1.28),2)'; @@ -296,7 +308,9 @@ class MathTest extends \PHPUnit\Framework\TestCase public function testFunctionsWithQuotes() { $calculator = new MathExecutor(); - $calculator->addFunction('concat', function($first, $second){return $first.$second;}); + $calculator->addFunction('concat', function ($first, $second) { + return $first . $second; + }); $this->assertEquals('testing', $calculator->execute('concat("test", "ing")')); $this->assertEquals('testing', $calculator->execute("concat('test', 'ing')")); } @@ -305,11 +319,10 @@ class MathTest extends \PHPUnit\Framework\TestCase { $calculator = new MathExecutor(); $testString = "some, long. arg; with: different-separators!"; - $calculator->addFunction('test', function ($arg) use ($testString) - { + $calculator->addFunction('test', function ($arg) use ($testString) { $this->assertEquals($testString, $arg); return 0; - } + } ); $calculator->execute('test("' . $testString . '")'); // single quotes $calculator->execute("test('" . $testString . "')"); // double quotes From 043058f3c72deae2f4a40eac7e91a03208eaba23 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Fri, 15 May 2020 21:52:35 +0300 Subject: [PATCH 03/14] Cleanup unused imports --- src/NXP/Classes/Calculator.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 21f3178..0db49e1 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -10,12 +10,6 @@ namespace NXP\Classes; -use NXP\Classes\Token\InterfaceOperator; -use NXP\Classes\Token\TokenFunction; -use NXP\Classes\Token\TokenNumber; -use NXP\Classes\Token\TokenStringDoubleQuoted; -use NXP\Classes\Token\TokenStringSingleQuoted; -use NXP\Classes\Token\TokenVariable; use NXP\Exception\IncorrectExpressionException; use NXP\Exception\UnknownFunctionException; use NXP\Exception\UnknownOperatorException; From e6f35cd802c611194901db7ca53c8151af931e22 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Fri, 15 May 2020 21:58:38 +0300 Subject: [PATCH 04/14] Fix version string for Travis --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e4ace7e..3098443 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": ">=7.1.*" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "~7.0" From b74742641f9c5dfb6077745c3c175bca6234d8e0 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Fri, 15 May 2020 22:02:52 +0300 Subject: [PATCH 05/14] 7.1 downgrade --- src/NXP/Classes/Tokenizer.php | 15 ++------------- src/NXP/MathExecutor.php | 2 -- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php index 869c720..b72b869 100644 --- a/src/NXP/Classes/Tokenizer.php +++ b/src/NXP/Classes/Tokenizer.php @@ -10,18 +10,6 @@ namespace NXP\Classes; -use NXP\Classes\Token\AbstractOperator; -use NXP\Classes\Token\InterfaceOperator; -use NXP\Classes\Token\InterfaceToken; -use NXP\Classes\Token\TokenComma; -use NXP\Classes\Token\TokenFunction; -use NXP\Classes\Token\TokenLeftBracket; -use NXP\Classes\Token\TokenMinus; -use NXP\Classes\Token\TokenNumber; -use NXP\Classes\Token\TokenRightBracket; -use NXP\Classes\Token\TokenStringDoubleQuoted; -use NXP\Classes\Token\TokenStringSingleQuoted; -use NXP\Classes\Token\TokenVariable; use NXP\Exception\IncorrectBracketsException; use NXP\Exception\UnknownOperatorException; use RuntimeException; @@ -80,7 +68,7 @@ class Tokenizer public function tokenize() { - foreach (mb_str_split($this->input, 1) as $ch) { + foreach (str_split($this->input, 1) as $ch) { switch (true) { case $this->inSingleQuotedString: if ($ch === "'") { @@ -111,6 +99,7 @@ class Tokenizer $this->numberBuffer .= $ch; $this->allowNegative = false; break; + /** @noinspection PhpMissingBreakStatementInspection */ case strtolower($ch) === "e": if ($this->numberBuffer != "" && strpos($this->numberBuffer, ".") !== false) { $this->numberBuffer .= "e"; diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index 8d3e890..a0a3f9b 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -14,8 +14,6 @@ namespace NXP; use NXP\Classes\Calculator; use NXP\Classes\CustomFunction; use NXP\Classes\Operator; -use NXP\Classes\Token\AbstractOperator; -use NXP\Classes\TokenFactory; use NXP\Classes\Tokenizer; use NXP\Exception\DivisionByZeroException; use ReflectionException; From 11ea95cb2125e329a84b30f05e25a79e29abf353 Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Fri, 15 May 2020 22:04:57 +0300 Subject: [PATCH 06/14] Fix readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 498d409..c665575 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ composer require nxp/math-executor ## Sample usage: ```php -use NXP\MathExecutor;require 'vendor/autoload.php'; +use NXP\MathExecutor; $executor = new MathExecutor(); @@ -84,7 +84,9 @@ Default operators: `+ - * / ^` Add custom operator to executor: ```php -use NXP\Classes\Operator;$executor->addOperator(new Operator( +use NXP\Classes\Operator; + +$executor->addOperator(new Operator( '%', // Operator sign false, // Is right associated operator 170, // Operator priority From 47ec6595468bed0ffbea3de9b8d9c4a68a9e0a14 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 21 Apr 2020 11:12:55 -0400 Subject: [PATCH 07/14] Negative expression start (#60) * Update documentation for PHPFUI/InstaDoc * Support for negative numbers starting paren enclosed expressions --- src/NXP/Classes/Lexer.php | 4 ++-- tests/MathTest.php | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/NXP/Classes/Lexer.php b/src/NXP/Classes/Lexer.php index a7fe697..9964de9 100644 --- a/src/NXP/Classes/Lexer.php +++ b/src/NXP/Classes/Lexer.php @@ -76,10 +76,10 @@ class Lexer $output[] = $token; } elseif ($token instanceof TokenNumber) { // if the number starts with a minus sign, it could be a negative number, or it could be an operator grabbed by the greedy regex - // if previous token is an operator, then it negative, otherwise remove the minus sign and put a negative operator on the stack + // if previous token is an operator or open bracket, then it negative, otherwise remove the minus sign and put a negative operator on the stack if ($lastToken !== null) { $value = $token->getValue(); - if (($value < 0 || $this->isNegativeZero($value)) && ! ($lastToken instanceof AbstractOperator)) { + if (($value < 0 || $this->isNegativeZero($value)) && ! ($lastToken instanceof AbstractOperator || $lastToken instanceof TokenLeftBracket)) { $token = new TokenNumber(abs($value)); $output[] = $token; $output[] = new TokenMinus('-'); diff --git a/tests/MathTest.php b/tests/MathTest.php index cc4cd1a..7cbc7de 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -114,6 +114,7 @@ class MathTest extends \PHPUnit\Framework\TestCase ['(5 + 3) * -1'], + ['-2- 2*2'], ['2- 2*2'], ['2-(2*2)'], ['(2- 2)*2'], @@ -155,6 +156,7 @@ class MathTest extends \PHPUnit\Framework\TestCase ['-1*-2'], ['(1+2+3+4-5)*7/100'], + ['(-1+2+3+4- 5)*7/100'], ['(1+2+3+4- 5)*7/100'], ['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'], @@ -176,6 +178,7 @@ class MathTest extends \PHPUnit\Framework\TestCase ['3 <= 5'], ['5 <= 5'], ['10 < 9 || 4 > (2+1)'], + ['10 < 9 || 4 > (-2+1)'], ['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'], ['1 + 5 == 3 + 1'], @@ -193,7 +196,11 @@ class MathTest extends \PHPUnit\Framework\TestCase ['(-4)'], ['(-4 + 5)'], - + ['(3 * 1)'], + ['(-3 * -1)'], + ['1 + (-3 * -1)'], + ['1 + ( -3 * 1)'], + ['1 + (3 * -1)'], ['1 - 0'], ['1-0'], ]; From f284316053cbd7cde8e5209585a9f974ef2321e7 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Thu, 14 May 2020 00:22:11 -0400 Subject: [PATCH 08/14] Doc update (#61) * Update documentation for PHPFUI/InstaDoc * Removing dead waffle.io links in docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d30e78..3ed5a95 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MathExecutor [![Stories in Ready](https://badge.waffle.io/NeonXP/MathExecutor.png?label=ready&title=Ready)](https://waffle.io/NeonXP/MathExecutor) [![Build Status](https://travis-ci.org/NeonXP/MathExecutor.png?branch=master)](https://travis-ci.org/NeonXP/MathExecutor) +# MathExecutor [![Build Status](https://travis-ci.org/NeonXP/MathExecutor.png?branch=master)](https://travis-ci.org/NeonXP/MathExecutor) # A simple and extensible math expressions calculator From 1bb9f61423c1eed320e95a1288a146ece410e1f9 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 21:52:26 -0400 Subject: [PATCH 09/14] typed parameters and return types --- src/NXP/Classes/Calculator.php | 2 +- src/NXP/Classes/CustomFunction.php | 4 ++-- src/NXP/Classes/Operator.php | 2 +- src/NXP/Classes/Tokenizer.php | 20 ++++++++++---------- src/NXP/MathExecutor.php | 18 +++++++++--------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 0db49e1..c6ccff1 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -49,7 +49,7 @@ class Calculator * @throws IncorrectExpressionException * @throws UnknownVariableException */ - public function calculate($tokens, $variables) + public function calculate(array $tokens, array $variables) { /** @var Token[] $stack */ $stack = []; diff --git a/src/NXP/Classes/CustomFunction.php b/src/NXP/Classes/CustomFunction.php index 944f7d9..636435c 100644 --- a/src/NXP/Classes/CustomFunction.php +++ b/src/NXP/Classes/CustomFunction.php @@ -32,7 +32,7 @@ class CustomFunction * @param int $places * @throws ReflectionException */ - public function __construct(string $name, callable $function, $places = null) + public function __construct(string $name, callable $function, int $places = null) { $this->name = $name; $this->function = $function; @@ -44,7 +44,7 @@ class CustomFunction } } - public function execute(&$stack) + public function execute(array &$stack) : Token { if (count($stack) < $this->places) { throw new IncorrectExpressionException(); diff --git a/src/NXP/Classes/Operator.php b/src/NXP/Classes/Operator.php index 53550a9..c3762ea 100644 --- a/src/NXP/Classes/Operator.php +++ b/src/NXP/Classes/Operator.php @@ -52,7 +52,7 @@ class Operator $this->places = $reflection->getNumberOfParameters(); } - public function execute(&$stack) + public function execute(array &$stack) : Token { if (count($stack) < $this->places) { throw new IncorrectExpressionException(); diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php index b72b869..caf395f 100644 --- a/src/NXP/Classes/Tokenizer.php +++ b/src/NXP/Classes/Tokenizer.php @@ -66,7 +66,7 @@ class Tokenizer $this->operators = $operators; } - public function tokenize() + public function tokenize() : self { foreach (str_split($this->input, 1) as $ch) { switch (true) { @@ -173,17 +173,17 @@ class Tokenizer return $this; } - private function isNumber($ch) + private function isNumber(string $ch) : bool { return $ch >= '0' && $ch <= '9'; } - private function isAlpha($ch) + private function isAlpha(string $ch) : bool { return $ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || $ch == '_'; } - private function emptyNumberBufferAsLiteral() + private function emptyNumberBufferAsLiteral() : void { if ($this->numberBuffer != "") { $this->tokens[] = new Token(Token::Literal, $this->numberBuffer); @@ -191,22 +191,22 @@ class Tokenizer } } - private function isDot($ch) + private function isDot(string $ch) : bool { return $ch == '.'; } - private function isLP($ch) + private function isLP(string $ch) : bool { return $ch == '('; } - private function isRP($ch) + private function isRP(string $ch) : bool { return $ch == ')'; } - private function emptyStrBufferAsVariable() + private function emptyStrBufferAsVariable() : void { if ($this->stringBuffer != "") { $this->tokens[] = new Token(Token::Variable, $this->stringBuffer); @@ -214,7 +214,7 @@ class Tokenizer } } - private function isComma($ch) + private function isComma(string $ch) : bool { return $ch == ','; } @@ -224,7 +224,7 @@ class Tokenizer * @throws IncorrectBracketsException * @throws UnknownOperatorException */ - public function buildReversePolishNotation() + public function buildReversePolishNotation() : array { $tokens = []; /** @var SplStack $stack */ diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index a0a3f9b..53d8dfd 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -58,7 +58,7 @@ class MathExecutor * Set default operands and functions * @throws ReflectionException */ - protected function addDefaults() + protected function addDefaults() : void { foreach ($this->defaultOperators() as $name => $operator) { list($callable, $priority, $isRightAssoc) = $operator; @@ -75,7 +75,7 @@ class MathExecutor * * @return array of class names */ - protected function defaultOperators() + protected function defaultOperators() : array { return [ '+' => [ @@ -102,7 +102,7 @@ class MathExecutor '/' => [ function ($a, $b) { if ($b == 0) { - throw new DivisionByZeroException(); + throw new DivisionByZeroException(); } return $a / $b; }, @@ -181,7 +181,7 @@ class MathExecutor * @param Operator $operator * @return MathExecutor */ - public function addOperator(Operator $operator) + public function addOperator(Operator $operator) : self { $this->operators[$operator->operator] = $operator; return $this; @@ -193,7 +193,7 @@ class MathExecutor * * @return array */ - protected function defaultFunctions() + protected function defaultFunctions() : array { return [ 'abs' => function ($arg) { @@ -341,9 +341,9 @@ class MathExecutor * @throws Exception\UnknownOperatorException * @throws Exception\UnknownVariableException */ - public function execute($expression) + public function execute(string $expression) { - $cachekey = (string)$expression; + $cachekey = $expression; if (!array_key_exists($cachekey, $this->cache)) { $tokens = (new Tokenizer($expression, $this->operators))->tokenize()->buildReversePolishNotation(); $this->cache[$cachekey] = $tokens; @@ -363,7 +363,7 @@ class MathExecutor * @return MathExecutor * @throws ReflectionException */ - public function addFunction($name, $function = null, $places = null) + public function addFunction(string $name, callable $function = null, int $places = null) : self { $this->functions[$name] = new CustomFunction($name, $function, $places); return $this; @@ -374,7 +374,7 @@ class MathExecutor * * @return array */ - protected function defaultVars() + protected function defaultVars() : array { return [ 'pi' => 3.14159265359, From ab3a44b33031d86c74cc8f07c9d904d8d20c7da9 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 22:20:56 -0400 Subject: [PATCH 10/14] Private members --- src/NXP/MathExecutor.php | 94 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index 53d8dfd..0b7e9db 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -15,6 +15,7 @@ use NXP\Classes\Calculator; use NXP\Classes\CustomFunction; use NXP\Classes\Operator; use NXP\Classes\Tokenizer; +use NXP\MathExecutorException; use NXP\Exception\DivisionByZeroException; use ReflectionException; @@ -29,17 +30,17 @@ class MathExecutor * * @var array */ - public $variables = []; + private $variables = []; /** * @var Operator[] */ - public $operators = []; + private $operators = []; /** * @var CustomFunction[] */ - public $functions = []; + private $functions = []; /** * @var array @@ -61,7 +62,7 @@ class MathExecutor protected function addDefaults() : void { foreach ($this->defaultOperators() as $name => $operator) { - list($callable, $priority, $isRightAssoc) = $operator; + [$callable, $priority, $isRightAssoc] = $operator; $this->addOperator(new Operator($name, $isRightAssoc, $priority, $callable)); } foreach ($this->defaultFunctions() as $name => $callable) { @@ -382,6 +383,91 @@ class MathExecutor ]; } + /** + * Get all vars + * + * @return array + */ + public function getVars() : array + { + return $this->variables; + } + + /** + * Get a specific var + * + * @param string $variable + * @return integer|float + * @throws UnknownVariableException + */ + public function getVar(string $variable) + { + if (!isset($this->variables[$variable])) { + throw new UnknownVariableException("Variable ({$variable}) not set"); + } + return $this->variables[$variable]; + } + + /** + * Add variable to executor + * + * @param string $variable + * @param integer|float $value + * @return MathExecutor + * @throws MathExecutorException + */ + public function setVar(string $variable, $value) : self + { + if (!is_numeric($value)) { + throw new MathExecutorException("Variable ({$variable}) value must be a number ({$value}) type ({gettype($value)})"); + } + $this->variables[$variable] = $value; + return $this; + } + + /** + * Remove variable from executor + * + * @param string $variable + * @return MathExecutor + */ + public function removeVar(string $variable) : self + { + unset ($this->variables[$variable]); + return $this; + } + + /** + * Remove all variables + * @return MathExecutor + */ + public function removeVars() : self + { + $this->variables = []; + return $this; + } + + /** + * Get all registered operators to executor + * + * @return array of operator class names + */ + public function getOperators() + { + return $this->tokenFactory->getOperators(); + } + + /** + * Get all registered functions + * + * @return array containing callback and places indexed by + * function name + */ + public function getFunctions() : array + { + return $this->tokenFactory->getFunctions(); + } + public function __clone() { $this->addDefaults(); From b95ab24f367baf928332dc5040ab444c1c719623 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 22:36:55 -0400 Subject: [PATCH 11/14] setDivisionByZeroIsZero --- src/NXP/MathExecutor.php | 16 ++++++++++++++++ tests/MathTest.php | 9 ++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index 0b7e9db..e214209 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -468,6 +468,22 @@ class MathExecutor return $this->tokenFactory->getFunctions(); } + /** + * Set division by zero returns zero instead of throwing DivisionByZeroException + * + * @return MathExecutor + */ + public function setDivisionByZeroIsZero() : self + { + $this->addOperator(new Operator("/", false, 180, function ($a, $b) { + if ($b == 0) { + return 0; + } + return $a / $b; + })); + return $this; + } + public function __clone() { $this->addDefaults(); diff --git a/tests/MathTest.php b/tests/MathTest.php index 958f14c..39ac649 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -203,7 +203,7 @@ class MathTest extends TestCase ['(-3 * -1)'], ['1 + (-3 * -1)'], ['1 + ( -3 * 1)'], - ['1 + (3 * -1)'], + ['1 + (3 *-1)'], ['1 - 0'], ['1-0'], ]; @@ -226,12 +226,7 @@ class MathTest extends TestCase public function testZeroDivision() { $calculator = new MathExecutor(); - $calculator->addOperator(new Operator("/", false, 180, function ($a, $b) { - if ($b == 0) { - return 0; - } - return $a / $b; - })); + $calculator->setDivisionByZeroIsZero(); $this->assertEquals(0, $calculator->execute('10 / 0')); } From 7343f2c9c4b8b7e72bfec22bce6c3df1b85624ab Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 22:37:16 -0400 Subject: [PATCH 12/14] Update readme.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c665575..14ede4c 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ $executor->addOperator(new Operator( $op2 = array_pop($stack); $op1 = array_pop($stack); $result = $op1->getValue() % $op2->getValue(); - + return $result; } )); @@ -124,15 +124,12 @@ $e = 2.71828182846 You can add your own variables to executor: ```php -$executor->setVars([ - 'var1' => 0.15, - 'var2' => 0.22 -]); +$executor->setVar('var1', 0.15)->setVar('var2', 0.22); echo $executor->execute("$var1 + $var2"); ``` ## Division By Zero Support: -Division by zero throws a `\NXP\Exception\DivisionByZeroException` +Division by zero throws a `\NXP\Exception\DivisionByZeroException` by default ```php try { echo $executor->execute('1/0'); @@ -140,12 +137,17 @@ try { echo $e->getMessage(); } ``` -If you want another behavior, you should override division operator: +Or call setDivisionByZeroIsZero +```php +echo $executor->setDivisionByZeroIsZero()->execute('1/0'); +``` + +If you want another behavior, you can override division operator: ```php $executor->addOperator("/", false, 180, function($a, $b) { if ($b == 0) { - return 0; + return null; } return $a / $b; }); From a621ea01c02109d2fd5808fe4e471b0dc86a5185 Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 22:59:10 -0400 Subject: [PATCH 13/14] Adding setVars back in --- src/NXP/MathExecutor.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index e214209..0c643a7 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -426,6 +426,25 @@ class MathExecutor } /** + * Add variables to executor + * + * @param array $variables + * @param bool $clear Clear previous variables + * @return MathExecutor + * @throws \Exception + */ + public function setVars(array $variables, bool $clear = true) : self + { + if ($clear) { + $this->removeVars(); + } + foreach ($variables as $name => $value) { + $this->setVar($name, $value); + } + return $this; + } + + /** * Remove variable from executor * * @param string $variable From d195b3e909ddcd2f983d303164a1855c3557c7ff Mon Sep 17 00:00:00 2001 From: Bruce Wells Date: Tue, 19 May 2020 23:00:10 -0400 Subject: [PATCH 14/14] Null parameters allowed --- src/NXP/MathExecutor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index 0c643a7..5a20e17 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -364,7 +364,7 @@ class MathExecutor * @return MathExecutor * @throws ReflectionException */ - public function addFunction(string $name, callable $function = null, int $places = null) : self + public function addFunction(string $name, ?callable $function = null, ?int $places = null) : self { $this->functions[$name] = new CustomFunction($name, $function, $places); return $this;