diff options
author | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2020-05-15 21:51:23 +0300 |
---|---|---|
committer | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2020-05-15 21:51:23 +0300 |
commit | cab8e2d38ae1c8c7fb75022f7d9b0539a0a86d4e (patch) | |
tree | d3107b0f586885d56b13dc65411b455a7aee37cb | |
parent | 01415abc9d7f7401d9f4c09fbbec24930c65a097 (diff) |
Massive refactoring
More clean structure
Parsing without regular expressions
38 files changed, 789 insertions, 1904 deletions
@@ -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 -<?php -namespace MyNamespace; - -use NXP\Classes\Token\AbstractOperator; - -class ModulusToken extends AbstractOperator -{ - /** - * Regex of this operator - * @return string - */ - public static function getRegex() +use NXP\Classes\Operator;$executor->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; /** @@ -25,35 +27,64 @@ 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 @@ +<?php + + +namespace NXP\Classes; + + +use NXP\Exception\IncorrectExpressionException; +use ReflectionException; +use ReflectionFunction; + +class CustomFunction +{ + /** + * @var string + */ + public $name; + + /** + * @var callable $function + */ + public $function; + + /** + * @var int + */ + public $places; + + /** + * CustomFunction constructor. + * @param string $name + * @param callable $function + * @param int $places + * @throws ReflectionException + */ + public function __construct(string $name, callable $function, $places = null) + { + $this->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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes; - -use NXP\Classes\Token\AbstractOperator; -use NXP\Classes\Token\InterfaceOperator; -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\IncorrectExpressionException; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ +<?php + + +namespace NXP\Classes; + + +use NXP\Exception\IncorrectExpressionException; +use ReflectionFunction; + +class Operator +{ + /** + * @var string + */ + public $operator; + + /** + * @var bool + */ + public $isRightAssoc; + + /** + * @var int + */ + public $priority; + + /** + * @var callable<\SplStack> + */ + 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 @@ +<?php + + +namespace NXP\Classes; + +class Token +{ + const Literal = "literal"; + const Variable = "variable"; + const Operator = "operator"; + const LeftParenthesis = "LP"; + const RightParenthesis = "RP"; + const Function = "function"; + const ParamSeparator = "separator"; + const String = "string"; + const Space = "space"; + + public $type = self::Literal; + + /** + * @var float|string + */ + public $value; + + /** + * Token constructor. + * @param string $type + * @param float|string $value + */ + public function __construct(string $type, $value) + { + $this->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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenAnd extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '&&'; - } - - /** - * @return int - */ - public function getPriority() - { - return 100; - } - - /** - * @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/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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** -* This file is part of the MathExecutor package -* -* (c) Alexander Kiryukhin -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code -*/ - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -/** -* @author Alexander Kiryukhin <a.kiryukhin@mail.ru> -*/ -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 @@ -<?php -/** -* This file is part of the MathExecutor package -* -* (c) Alexander Kiryukhin -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code -*/ - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; -use NXP\Exception\DivisionByZeroException; - -/** -* @author Alexander Kiryukhin <a.kiryukhin@mail.ru> -*/ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenEqual extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\=\='; - } - - /** - * @return int - */ - public function getPriority() - { - return 140; - } - - /** - * @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/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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenGreaterThan extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '>'; - } - - /** - * @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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenGreaterThanOrEqual extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '>\='; - } - - /** - * @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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenLessThan extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '<'; - } - - /** - * @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/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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenLessThanOrEqual extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '<\='; - } - - /** - * @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/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 @@ -<?php -/** -* This file is part of the MathExecutor package -* -* (c) Alexander Kiryukhin -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code -*/ - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -/** -* @author Alexander Kiryukhin <a.kiryukhin@mail.ru> -*/ -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 @@ -<?php -/** -* This file is part of the MathExecutor package -* -* (c) Alexander Kiryukhin -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code -*/ - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -/** -* @author Alexander Kiryukhin <a.kiryukhin@mail.ru> -*/ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenNotEqual extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\!\='; - } - - /** - * @return int - */ - public function getPriority() - { - return 140; - } - - /** - * @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/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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -class TokenOr extends AbstractOperator -{ - /** - * @return string - */ - public static function getRegex() - { - return '\|\|'; - } - - /** - * @return int - */ - public function getPriority() - { - return 90; - } - - /** - * @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/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 @@ -<?php -/** -* This file is part of the MathExecutor package -* -* (c) Alexander Kiryukhin -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code -*/ - -namespace NXP\Classes\Token; - -use NXP\Exception\IncorrectExpressionException; - -/** -* @author Alexander Kiryukhin <a.kiryukhin@mail.ru> -*/ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Bruce Wells <brucekwells@gmail.com> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Bruce Wells <brucekwells@gmail.com> - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes\Token; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ -<?php -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Classes; - -use NXP\Classes\Token\InterfaceToken; -use NXP\Classes\Token\TokenComma; -use NXP\Classes\Token\TokenFunction; -use NXP\Classes\Token\TokenLeftBracket; -use NXP\Classes\Token\TokenNumber; -use NXP\Classes\Token\TokenRightBracket; -use NXP\Classes\Token\TokenStringSingleQuoted; -use NXP\Classes\Token\TokenVariable; -use NXP\Classes\Token\TokenStringDoubleQuoted; -use NXP\Exception\UnknownFunctionException; -use NXP\Exception\UnknownOperatorException; -use NXP\Exception\UnknownTokenException; - -/** - * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> - */ -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 @@ +<?php +/** + * This file is part of the MathExecutor package + * + * (c) Alexander Kiryukhin + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code + */ + +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; +use SplStack; + +/** + * @author Alexander Kiryukhin <a.kiryukhin@mail.ru> + */ +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<Token> $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 @@ -<?php - -/** - * This file is part of the MathExecutor package - * - * (c) Alexander Kiryukhin - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code - */ - -namespace NXP\Exception; - -/** - * @author Vitaliy Zhuk <zhuk2205@gmail.com> - */ -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,240 +56,140 @@ 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 + * Set default operands and functions + * @throws ReflectionException */ - public function setVars(array $variables, $clear = true) + protected function addDefaults() { - if ($clear) { - $this->removeVars(); + foreach ($this->defaultOperators() as $name => $operator) { + list($callable, $priority, $isRightAssoc) = $operator; + $this->addOperator(new Operator($name, $isRightAssoc, $priority, $callable)); } - - foreach ($variables as $name => $value) { - $this->setVar($name, $value); + foreach ($this->defaultFunctions() as $name => $callable) { + $this->addFunction($name, $callable); } - - return $this; + $this->variables = $this->defaultVars(); } /** - * Remove variable from executor + * Get the default operators * - * @param string $variable - * @return MathExecutor - */ - public function removeVar($variable) - { - unset ($this->variables[$variable]); - - return $this; - } - - /** - * Remove all variables - * @return MathExecutor + * @return array of class names */ - public function removeVars() + protected function defaultOperators() { - $this->variables = []; - - return $this; + return [ + '+' => [ + 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 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 + * @param Operator $operator * @return MathExecutor */ - public function setDivisionByZeroException($exception = true) + public function addOperator(Operator $operator) { - $this->tokenFactory->setDivisionByZeroException($exception); + $this->operators[$operator->operator] = $operator; 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 - */ - protected function addDefaults() - { - $this->tokenFactory = new TokenFactory(); - - foreach ($this->defaultOperators() as $operatorClass) { - $this->tokenFactory->addOperator($operatorClass); - } - - foreach ($this->defaultFunctions() as $name => $callable) { - $this->tokenFactory->addFunction($name, $callable); - } - - $this->setVars($this->defaultVars()); - } - - /** - * Get the default operators - * - * @return array of class names - */ - 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, - ]; - } - - /** * Gets the default functions as an array. Key is function name * and value is the function as a closure. * @@ -425,6 +334,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 * * @return array @@ -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 |