From eb9c3651614dd5e5aef067880092e9f622c264df Mon Sep 17 00:00:00 2001 From: zhukv Date: Sat, 3 Aug 2013 13:47:47 +0300 Subject: [PATCH] Fix to PSR standart, fix tokenizer, fix function executor. --- .travis.yml | 9 + LICENSE | 19 ++ NXP/Tests/MathTest.php | 64 ------ README.md | 50 ++++- composer.json | 2 +- phpunit.xml.dist | 20 ++ {NXP => src/NXP}/Classes/Func.php | 18 +- {NXP => src/NXP}/Classes/Operand.php | 18 +- {NXP => src/NXP}/Classes/Token.php | 19 +- {NXP => src/NXP}/Classes/TokenParser.php | 58 +++--- .../IncorrectExpressionException.php | 19 ++ src/NXP/Exception/MathExecutorException.php | 19 ++ .../Exception/UnknownFunctionException.php | 19 ++ .../Exception/UnknownOperatorException.php | 19 ++ src/NXP/Exception/UnknownTokenException.php | 19 ++ {NXP => src/NXP}/MathExecutor.php | 195 +++++++++++++++--- test.php | 12 -- tests/MathTest.php | 51 +++++ tests/bootstrap.php | 11 + 19 files changed, 476 insertions(+), 165 deletions(-) create mode 100644 .travis.yml create mode 100644 LICENSE delete mode 100644 NXP/Tests/MathTest.php create mode 100644 phpunit.xml.dist rename {NXP => src/NXP}/Classes/Func.php (61%) rename {NXP => src/NXP}/Classes/Operand.php (79%) rename {NXP => src/NXP}/Classes/Token.php (72%) rename {NXP => src/NXP}/Classes/TokenParser.php (86%) create mode 100644 src/NXP/Exception/IncorrectExpressionException.php create mode 100644 src/NXP/Exception/MathExecutorException.php create mode 100644 src/NXP/Exception/UnknownFunctionException.php create mode 100644 src/NXP/Exception/UnknownOperatorException.php create mode 100644 src/NXP/Exception/UnknownTokenException.php rename {NXP => src/NXP}/MathExecutor.php (64%) delete mode 100644 test.php create mode 100644 tests/MathTest.php create mode 100644 tests/bootstrap.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..842031b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: php + +php: + - 5.3 + - 5.4 + +before_script: + - wget http://getcomposer.org/composer.phar + - php composer.phar install \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e45f709 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Alexander Kiryukhin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/NXP/Tests/MathTest.php b/NXP/Tests/MathTest.php deleted file mode 100644 index 6bfcf55..0000000 --- a/NXP/Tests/MathTest.php +++ /dev/null @@ -1,64 +0,0 @@ -generateExpression(); - print "Test #$i. Expression: '$expression'\t"; - - eval('$result1 = ' . $expression . ';'); - print "PHP result: $result1 \t"; - $result2 = $calculator->execute($expression); - print "NXP Math Executor result: $result2\n"; - $this->assertEquals($result1, $result2); - } - } - - private function generateExpression() - { - $operators = [ '+', '-', '*', '/' ]; - $number = true; - $expression = ''; - $brackets = 0; - for ($i = 1; $i < rand(1,10)*2; $i++) { - if ($number) { - $expression .= rand(1,100)*0.5; - } else { - $expression .= $operators[rand(0,3)]; - } - $number = !$number; - $rand = rand(1,5); - if (($rand == 1) && ($number)) { - $expression .= '('; - $brackets++; - } elseif (($rand == 2) && (!$number) && ($brackets > 0)) { - $expression .= ')'; - $brackets--; - } - } - if ($number) { - $expression .= rand(1,100)*0.5; - } - $expression .= str_repeat(')', $brackets); - - return $expression; - } -} \ No newline at end of file diff --git a/README.md b/README.md index 8cbbaa8..ca5ea65 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,49 @@ Simple math expressions calculator -## Sample usage: - - execute("1 + 2 * (2 - (4+10))^2"); - ## Install via Composer All instructions to install here: https://packagist.org/packages/nxp/math-executor + +## Sample usage: + +```php +require "vendor/autoload.php"; + +$calculator = new \NXP\MathExecutor(); + +print $calculator->execute("1 + 2 * (2 - (4+10))^2"); +``` + +## Functions: + +Default functions: +* sin +* cos +* tn +* asin +* asoc +* atn + +Add custom function to executor: +```php +$executor->addFunction('abs', function($arg) { + return abs($arg); +}); +``` + +## Operators: + +Default operators: `+ - * / ^` + +## Variables: + +You can add own variable to executor: + +```php +$executor->setVars(array( + 'var1' => 0.15, + 'var2' => 0.22 +)); + +$executor->execute("var1 + var2"); \ No newline at end of file diff --git a/composer.json b/composer.json index 701b47c..586ad96 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,6 @@ } ], "autoload": { - "psr-0": {"NXP": "."} + "psr-0": {"NXP": "src/"} } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..bfe1846 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + + ./tests/ + + + \ No newline at end of file diff --git a/NXP/Classes/Func.php b/src/NXP/Classes/Func.php similarity index 61% rename from NXP/Classes/Func.php rename to src/NXP/Classes/Func.php index b3e5b93..e8c0fa2 100644 --- a/NXP/Classes/Func.php +++ b/src/NXP/Classes/Func.php @@ -1,14 +1,18 @@ name = $name; $this->callback = $callback; @@ -38,4 +42,4 @@ class Func { { return $this->callback; } -} \ No newline at end of file +} diff --git a/NXP/Classes/Operand.php b/src/NXP/Classes/Operand.php similarity index 79% rename from NXP/Classes/Operand.php rename to src/NXP/Classes/Operand.php index 3329679..fae0c69 100644 --- a/NXP/Classes/Operand.php +++ b/src/NXP/Classes/Operand.php @@ -1,14 +1,18 @@ association = $association; $this->symbol = $symbol; @@ -82,4 +86,4 @@ class Operand { return $this->priority; } -} \ No newline at end of file +} diff --git a/NXP/Classes/Token.php b/src/NXP/Classes/Token.php similarity index 72% rename from NXP/Classes/Token.php rename to src/NXP/Classes/Token.php index 28bc736..bbcd5bc 100644 --- a/NXP/Classes/Token.php +++ b/src/NXP/Classes/Token.php @@ -1,15 +1,18 @@ type = $type; $this->value = $value; @@ -50,4 +53,4 @@ class Token { return $this->value; } -} \ No newline at end of file +} diff --git a/NXP/Classes/TokenParser.php b/src/NXP/Classes/TokenParser.php similarity index 86% rename from NXP/Classes/TokenParser.php rename to src/NXP/Classes/TokenParser.php index 7b9255f..f498184 100644 --- a/NXP/Classes/TokenParser.php +++ b/src/NXP/Classes/TokenParser.php @@ -1,14 +1,18 @@ '[0-9\.]', - self::CHAR => '[a-z]', + self::CHAR => '[a-z_]', self::SPECIAL_CHAR => '[\!\@\#\$\%\^\&\*\/\|\-\+\=\~]', self::LEFT_BRACKET => '\(', self::RIGHT_BRACKET => '\)', self::SPACE => '\s' - ]; + ); const ERROR_STATE = 'ERROR_STATE'; - private $transitions = [ - Token::NOTHING => [ + private $transitions = array( + Token::NOTHING => array( self::DIGIT => Token::NUMBER, self::CHAR => Token::STRING, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - Token::STRING => [ + ), + Token::STRING => array( self::DIGIT => Token::STRING, self::CHAR => Token::STRING, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - Token::NUMBER => [ + ), + Token::NUMBER => array( self::DIGIT => Token::NUMBER, self::CHAR => self::ERROR_STATE, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - Token::OPERATOR => [ + ), + Token::OPERATOR => array( self::DIGIT => Token::NUMBER, self::CHAR => Token::STRING, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - self::ERROR_STATE => [ + ), + self::ERROR_STATE => array( self::DIGIT => self::ERROR_STATE, self::CHAR => self::ERROR_STATE, self::SPECIAL_CHAR => self::ERROR_STATE, self::LEFT_BRACKET => self::ERROR_STATE, self::RIGHT_BRACKET => self::ERROR_STATE, self::SPACE => self::ERROR_STATE - ], - Token::LEFT_BRACKET => [ + ), + Token::LEFT_BRACKET => array( self::DIGIT => Token::NUMBER, self::CHAR => Token::STRING, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - Token::RIGHT_BRACKET => [ + ), + Token::RIGHT_BRACKET => array( self::DIGIT => Token::NUMBER, self::CHAR => Token::STRING, self::SPECIAL_CHAR => Token::OPERATOR, self::LEFT_BRACKET => Token::LEFT_BRACKET, self::RIGHT_BRACKET => Token::RIGHT_BRACKET, self::SPACE => Token::NOTHING - ], - ]; + ), + ); private $accumulator = ''; @@ -92,7 +96,7 @@ class TokenParser { private $queue = null; - function __construct() + public function __construct() { $this->queue = new \SplQueue(); } @@ -148,12 +152,14 @@ class TokenParser { { if ($oldState == Token::NOTHING) { $this->accumulator = ''; + return; } + if (($this->state != $oldState) || ($oldState == Token::LEFT_BRACKET) || ($oldState == Token::RIGHT_BRACKET)) { $token = new Token($oldState, $this->accumulator); $this->queue->push($token); $this->accumulator = ''; } } -} \ No newline at end of file +} diff --git a/src/NXP/Exception/IncorrectExpressionException.php b/src/NXP/Exception/IncorrectExpressionException.php new file mode 100644 index 0000000..ad5bc42 --- /dev/null +++ b/src/NXP/Exception/IncorrectExpressionException.php @@ -0,0 +1,19 @@ + + */ +class IncorrectExpressionException extends \Exception +{ +} diff --git a/src/NXP/Exception/MathExecutorException.php b/src/NXP/Exception/MathExecutorException.php new file mode 100644 index 0000000..0e3ea84 --- /dev/null +++ b/src/NXP/Exception/MathExecutorException.php @@ -0,0 +1,19 @@ + + */ +abstract class MathExecutorException extends \Exception +{ +} diff --git a/src/NXP/Exception/UnknownFunctionException.php b/src/NXP/Exception/UnknownFunctionException.php new file mode 100644 index 0000000..5bb3658 --- /dev/null +++ b/src/NXP/Exception/UnknownFunctionException.php @@ -0,0 +1,19 @@ + + */ +class UnknownFunctionException extends \Exception +{ +} diff --git a/src/NXP/Exception/UnknownOperatorException.php b/src/NXP/Exception/UnknownOperatorException.php new file mode 100644 index 0000000..b6617c3 --- /dev/null +++ b/src/NXP/Exception/UnknownOperatorException.php @@ -0,0 +1,19 @@ + + */ +class UnknownOperatorException extends \Exception +{ +} diff --git a/src/NXP/Exception/UnknownTokenException.php b/src/NXP/Exception/UnknownTokenException.php new file mode 100644 index 0000000..b8a593f --- /dev/null +++ b/src/NXP/Exception/UnknownTokenException.php @@ -0,0 +1,19 @@ + + */ +class UnknownTokenException extends \Exception +{ +} diff --git a/NXP/MathExecutor.php b/src/NXP/MathExecutor.php similarity index 64% rename from NXP/MathExecutor.php rename to src/NXP/MathExecutor.php index cc1c187..482b4b7 100644 --- a/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -1,28 +1,51 @@ addDefaults(); + } + + public function __clone() + { + $this->variables = array(); + $this->operators = array(); + $this->functions = array(); + + $this->addDefaults(); + } + + /** + * Set default operands and functions + */ + protected function addDefaults() { $this->addOperator(new Operand('+', 1, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1+$op2; })); $this->addOperator(new Operand('-', 1, Operand::LEFT_ASSOCIATED, Operand::BINARY, function ($op1, $op2) { return $op1-$op2; })); @@ -55,38 +95,100 @@ class MathExecutor { /** * Add operator to executor - * @param Operand $operator + * + * @param Operand $operator + * @return MathExecutor */ public function addOperator(Operand $operator) { $this->operators[$operator->getSymbol()] = $operator; + + return $this; } /** * Add function to executor - * @param Func $function + * + * @param string $name + * @param callable $function + * @return MathExecutor */ - public function addFunction(Func $function) + public function addFunction($name, callable $function = null) { - $this->functions[$function->getName()] = $function->getCallback(); + if ($name instanceof Func) { + $this->functions[$name->getName()] = $name->getCallback(); + } else { + $this->functions[$name] = $function; + } + + return $this; } /** * Add variable to executor - * @param $variable - * @param $value + * + * @param string $variable + * @param integer|float $value * @throws \Exception + * @return MathExecutor */ public function setVar($variable, $value) { if (!is_numeric($value)) { throw new \Exception("Variable value must be a number"); } + $this->variables[$variable] = $value; + + return $this; + } + + /** + * Add variables to executor + * + * @param array $variables + * @param bool $clear Clear previous variables + * @return MathExecutor + */ + 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 + */ + public function removeVars() + { + $this->variables = array(); + + return $this; } /** * Execute expression + * * @param $expression * @return int|float */ @@ -100,6 +202,7 @@ class MathExecutor { /** * Convert expression from normal expression form to RPN + * * @param $expression * @return \SplQueue * @throws \Exception @@ -118,9 +221,11 @@ class MathExecutor { while (!$this->stack->isEmpty()) { $token = $this->stack->pop(); + if ($token->getType() != Token::OPERATOR) { throw new \Exception('Opening bracket without closing bracket'); } + $this->queue->push($token); } @@ -128,7 +233,7 @@ class MathExecutor { } /** - * @param Token $token + * @param Token $token * @throws \Exception */ private function categorizeToken(Token $token) @@ -157,17 +262,23 @@ class MathExecutor { $previousToken = $this->stack->pop(); } if ((!$this->stack->isEmpty()) && ($this->stack->top()->getType() == Token::STRING)) { - $string = $this->stack->pop()->getValue(); - if (!array_key_exists($string, $this->functions)) { - throw new \Exception('Unknown function'); + $funcName = $this->stack->pop()->getValue(); + if (!array_key_exists($funcName, $this->functions)) { + throw new UnknownFunctionException(sprintf( + 'Unknown function: "%s".', + $funcName + )); } - $this->queue->push(new Token(Token::FUNC, $string)); + $this->queue->push(new Token(Token::FUNC, $funcName)); } break; case Token::OPERATOR: if (!array_key_exists($token->getValue(), $this->operators)) { - throw new \Exception("Unknown operator '{$token->getValue()}'"); + throw new UnknownOperatorException(sprintf( + 'Unknown operator: "%s".', + $token->getValue() + )); } $this->proceedOperator($token); @@ -175,7 +286,10 @@ class MathExecutor { break; default: - throw new \Exception('Unknown token'); + throw new UnknownTokenException(sprintf( + 'Unknown token: "%s".', + $token->getValue() + )); } } @@ -183,17 +297,25 @@ class MathExecutor { * @param $token * @throws \Exception */ - private function proceedOperator($token) + private function proceedOperator(Token $token) { if (!array_key_exists($token->getValue(), $this->operators)) { - throw new \Exception('Unknown operator'); + throw new UnknownOperatorException(sprintf( + 'Unknown operator: "%s".', + $token->getValue() + )); } + /** @var Operand $operator */ $operator = $this->operators[$token->getValue()]; + while (!$this->stack->isEmpty()) { $top = $this->stack->top(); + if ($top->getType() == Token::OPERATOR) { - $priority = $this->operators[$top->getValue()]->getPriority(); + /** @var Operand $operator */ + $operator = $this->operators[$top->getValue()]; + $priority = $operator->getPriority(); if ( $operator->getAssociation() == Operand::RIGHT_ASSOCIATED) { if (($priority > $operator->getPriority())) { $this->queue->push($this->stack->pop()); @@ -216,19 +338,20 @@ class MathExecutor { } /** - * @param \SplQueue $expression + * @param \SplQueue $expression * @return mixed * @throws \Exception */ private function calculateReversePolishNotation(\SplQueue $expression) { $this->stack = new \SplStack(); - /** @val Token $token */ + /** @var Token $token */ foreach ($expression as $token) { switch ($token->getType()) { case Token::NUMBER : $this->stack->push($token); break; + case Token::OPERATOR: /** @var Operand $operator */ $operator = $this->operators[$token->getValue()]; @@ -241,24 +364,30 @@ class MathExecutor { } $callback = $operator->getCallback(); - - $this->stack->push(new Token(Token::NUMBER, ($callback($arg1, $arg2)))); + $this->stack->push(new Token(Token::NUMBER, (call_user_func($callback, $arg1, $arg2)))); break; + case Token::FUNC: /** @var Func $function */ $callback = $this->functions[$token->getValue()]; $arg = $this->stack->pop()->getValue(); - $this->stack->push(new Token(Token::NUMBER, ($callback($arg)))); + $this->stack->push(new Token(Token::NUMBER, (call_user_func($callback, $arg)))); break; + default: - throw new \Exception('Unknown token'); + throw new UnknownTokenException(sprintf( + 'Unknown token: "%s".', + $token->getValue() + )); } } + $result = $this->stack->pop()->getValue(); + if (!$this->stack->isEmpty()) { - throw new \Exception('Incorrect expression'); + throw new IncorrectExpressionException('Incorrect expression.'); } return $result; } -} \ No newline at end of file +} diff --git a/test.php b/test.php deleted file mode 100644 index 2e5d0e7..0000000 --- a/test.php +++ /dev/null @@ -1,12 +0,0 @@ -execute("1 + 2 * (2 - (4+10))^2"); -var_dump($r); diff --git a/tests/MathTest.php b/tests/MathTest.php new file mode 100644 index 0000000..ba944fe --- /dev/null +++ b/tests/MathTest.php @@ -0,0 +1,51 @@ +assertEquals($calculator->execute($expression), $phpResult); + } + + /** + * Expressions data provider + */ + public function providerExpressions() + { + return array( + array('0.1 + 0.2'), + array('1 + 2'), + + array('0.1 - 0.2'), + array('1 - 2'), + + array('0.1 * 2'), + array('1 * 2'), + + array('0.1 / 0.2'), + array('1 / 2'), + + array('1 + 0.6 - (3 * 2 / 50)') + ); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..8eb3e7a --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +