Fix to PSR standart, fix tokenizer, fix function executor.

This commit is contained in:
zhukv 2013-08-03 13:47:47 +03:00
parent 253fb694a3
commit eb9c365161
19 changed files with 476 additions and 165 deletions

9
.travis.yml Normal file
View file

@ -0,0 +1,9 @@
language: php
php:
- 5.3
- 5.4
before_script:
- wget http://getcomposer.org/composer.phar
- php composer.phar install

19
LICENSE Normal file
View file

@ -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.

View file

@ -1,64 +0,0 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 14.03.13
* Time: 3:41
*/
namespace NXP\Tests;
use \NXP\MathExecutor;
class MathTest extends \PHPUnit_Framework_TestCase {
public function setup()
{
require '../MathExecutor.php';
require '../Classes/Func.php';
require '../Classes/Operand.php';
require '../Classes/Token.php';
require '../Classes/TokenParser.php';
}
public function testCalculating()
{
$calculator = new MathExecutor();
for ($i = 1; $i <= 10; $i++) {
$expression = $this->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;
}
}

View file

@ -2,13 +2,49 @@
Simple math expressions calculator
## Sample usage:
<?php
require "vendor/autoload.php";
$calculator = new \NXP\MathExecutor();
print $calculator->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");

View file

@ -12,6 +12,6 @@
}
],
"autoload": {
"psr-0": {"NXP": "."}
"psr-0": {"NXP": "src/"}
}
}

20
phpunit.xml.dist Normal file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php"
>
<testsuites>
<testsuite name="Math Executor tests">
<directory>./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -1,14 +1,18 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 17.03.13
* Time: 4:30
* 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;
class Func {
class Func
{
/**
* @var string
*/
@ -23,7 +27,7 @@ class Func {
* @param $name
* @param $callback
*/
function __construct($name, $callback)
public function __construct($name, $callback)
{
$this->name = $name;
$this->callback = $callback;
@ -38,4 +42,4 @@ class Func {
{
return $this->callback;
}
}
}

View file

@ -1,14 +1,18 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 17.03.13
* Time: 4:27
* 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;
class Operand {
class Operand
{
const LEFT_ASSOCIATED = 'LEFT_ASSOCIATED';
const RIGHT_ASSOCIATED = 'RIGHT_ASSOCIATED';
const ASSOCIATED = 'ASSOCIATED';
@ -48,7 +52,7 @@ class Operand {
* @param $type
* @param $callback
*/
function __construct($symbol, $priority, $association, $type, $callback)
public function __construct($symbol, $priority, $association, $type, $callback)
{
$this->association = $association;
$this->symbol = $symbol;
@ -82,4 +86,4 @@ class Operand {
return $this->priority;
}
}
}

View file

@ -1,15 +1,18 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 17.03.13
* Time: 3:23
* 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;
class Token {
class Token
{
const NOTHING = 'NOTHING';
const STRING = 'STRING';
const NUMBER = 'NUMBER';
@ -28,7 +31,7 @@ class Token {
*/
protected $type;
function __construct($type, $value)
public function __construct($type, $value)
{
$this->type = $type;
$this->value = $value;
@ -50,4 +53,4 @@ class Token {
return $this->value;
}
}
}

View file

@ -1,14 +1,18 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 17.03.13
* Time: 2:45
* 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;
class TokenParser {
class TokenParser
{
const DIGIT = 'DIGIT';
const CHAR = 'CHAR';
const SPECIAL_CHAR = 'SPECIAL_CHAR';
@ -16,75 +20,75 @@ class TokenParser {
const RIGHT_BRACKET = 'RIGHT_BRACKET';
const SPACE = 'SPACE';
private $terms = [
private $terms = array(
self::DIGIT => '[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 = '';
}
}
}
}

View file

@ -0,0 +1,19 @@
<?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 IncorrectExpressionException extends \Exception
{
}

View file

@ -0,0 +1,19 @@
<?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>
*/
abstract class MathExecutorException extends \Exception
{
}

View file

@ -0,0 +1,19 @@
<?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 UnknownFunctionException extends \Exception
{
}

View file

@ -0,0 +1,19 @@
<?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 UnknownOperatorException extends \Exception
{
}

View file

@ -0,0 +1,19 @@
<?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 \Exception
{
}

View file

@ -1,28 +1,51 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 14.03.13
* Time: 1:01
* 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;
use NXP\Classes\Func;
use NXP\Classes\Operand;
use NXP\Classes\Token;
use NXP\Classes\TokenParser;
use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\UnknownFunctionException;
use NXP\Exception\UnknownOperatorException;
use NXP\Exception\UnknownTokenException;
/**
* Class MathExecutor
* @package NXP
*/
class MathExecutor {
class MathExecutor
{
/**
* Available operators
*
* @var array
*/
private $operators = array();
/**
* Available functions
*
* @var array
*/
private $functions = array();
private $operators = [ ];
private $functions = [ ];
private $variables = [ ];
/**
* Available variables
*
* @var array
*/
private $variables = array();
/**
* @var \SplStack
@ -38,6 +61,23 @@ class MathExecutor {
* Base math operators
*/
public function __construct()
{
$this->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;
}
}
}

View file

@ -1,12 +0,0 @@
<?php
/**
* Author: Alexander "NeonXP" Kiryukhin
* Date: 14.03.13
* Time: 1:08
*/
require "vendor/autoload.php";
$e = new \NXP\MathExecutor();
$r = $e->execute("1 + 2 * (2 - (4+10))^2");
var_dump($r);

51
tests/MathTest.php Normal file
View file

@ -0,0 +1,51 @@
<?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\Tests;
use \NXP\MathExecutor;
class MathTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider providerExpressions
*/
public function testCalculating($expression)
{
$calculator = new MathExecutor();
/** @var float $phpResult */
eval('$phpResult = ' . $expression . ';');
$this->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)')
);
}
}

11
tests/bootstrap.php Normal file
View file

@ -0,0 +1,11 @@
<?php
$vendorDir = __DIR__ . '/../../..';
if (file_exists($file = $vendorDir . '/autoload.php')) {
require_once $file;
} else if (file_exists($file = './vendor/autoload.php')) {
require_once $file;
} else {
throw new \RuntimeException("Not found composer autoload");
}