# Conflicts:
#	src/NXP/Classes/Lexer.php
This commit is contained in:
Bruce Wells 2019-01-11 21:45:29 -05:00
commit 145a0a136f
28 changed files with 239 additions and 127 deletions

View file

@ -2,7 +2,9 @@ language: php
php:
- 5.6
- 7.1
- 7.2
- 7.3
before_script:
- wget http://getcomposer.org/composer.phar

View file

@ -11,18 +11,13 @@ A simple math expressions calculator
* Exceptions on divide by zero, or treat as zero
* Unary Minus (e.g. -3)
* Pi ($pi) and Euler's number ($e) support to 11 decimal places
* Easily extendable
## Install via Composer:
Stable branch
```
composer require "nxp/math-executor"
```
Dev branch (currently unsupported)
```
composer require "nxp/math-executor" "dev-dev"
```
## Sample usage:
```php
require "vendor/autoload.php";
@ -132,7 +127,7 @@ $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 thow a \NXP\Exception\DivisionByZeroException by calling setDivisionByZeroException.
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`.
```php
$executor->setDivisionByZeroException();
@ -144,7 +139,7 @@ try {
```
## Unary Minus Operator:
Negative numbers are supported via the unary minus operator, but need to have a space before the minus sign. `-1+ -3` is legal, while '`-1+-3` will produce an error due to the way the parser works. Positive numbers are not explicitly supported as unsigned numbers are assumed positive.
Negative numbers are supported via the unary minus operator, but need to have a space before the minus sign. `-1+ -3` is legal, while `-1+-3` will produce an error due to the way the parser works. Positive numbers are not explicitly supported as unsigned numbers are assumed positive.
## String Support:
Expressions can contain double or single quoted strings that are evaluated the same way as PHP evalutes strings as numbers. You can also pass strings to functions.
@ -152,3 +147,13 @@ Expressions can contain double or single quoted strings that are evaluated the s
```php
echo $executor->execute("1 + '2.5' * '.5' + myFunction('category')");
```
## Extending MathExecutor
You can add operators, functions and variables with the public methods in MathExecutor, but if you need to do more serious modifications to base behaviours, the easiest way to extend MathExecutor is to redefine the following methods in your derived class:
* defaultOperators
* defaultFunctions
* defaultVars
This will allow you to remove functions and operators if needed, or implement different types more simply.
Also note that you can replace an existing default operator by adding a new operator with the same regular expression string. For example if you just need to redefine TokenPlus, you can just add a new operator with the same regex string, in this case '\\+'.

9
code-of-conduct.md Normal file
View file

@ -0,0 +1,9 @@
# Code of conduct
We don't care who you are IRL. Be professional and responsible.
If you are a good person, we are happy for you.
If you are an asshole to us, we will be assholes in relation to you.
> Do to no one what you yourself dislike

View file

@ -13,13 +13,14 @@ namespace NXP\Classes;
use NXP\Classes\Token\InterfaceOperator;
use NXP\Classes\Token\TokenFunction;
use NXP\Classes\Token\TokenNumber;
use NXP\Classes\Token\TokenString;
use NXP\Classes\Token\TokenStringSingleQuoted;
use NXP\Classes\Token\TokenStringDoubleQuoted;
use NXP\Classes\Token\TokenVariable;
use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\UnknownVariableException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class Calculator
{
@ -38,7 +39,10 @@ class Calculator
if ($token instanceof TokenNumber) {
array_push($stack, $token);
}
if ($token instanceof TokenString) {
if ($token instanceof TokenStringDoubleQuoted) {
array_push($stack, $token);
}
if ($token instanceof TokenStringSingleQuoted) {
array_push($stack, $token);
}
if ($token instanceof TokenVariable) {

View file

@ -17,13 +17,14 @@ 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\TokenString;
use NXP\Classes\Token\TokenStringDoubleQuoted;
use NXP\Exception\IncorrectBracketsException;
use NXP\Exception\IncorrectExpressionException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class Lexer
{
@ -38,9 +39,8 @@ class Lexer
}
/**
* @param string $input Source string of equation
* @return array Tokens stream
* @throws \NXP\Exception\IncorrectExpressionException
* @param string $input Source string of equation
* @return array Tokens stream
*/
public function stringToTokensStream($input)
{
@ -58,9 +58,9 @@ class Lexer
}
/**
* @param array $tokensStream Tokens stream
* @return array Array of tokens in revers polish notation
* @throws \NXP\Exception\IncorrectExpressionException
* @param array $tokensStream Tokens stream
* @return array Array of tokens in revers polish notation
* @throws IncorrectBracketsException
*/
public function buildReversePolishNotation($tokensStream)
{
@ -68,54 +68,50 @@ class Lexer
$stack = [];
foreach ($tokensStream as $token) {
if ($token instanceof TokenString) {
if ($token instanceof TokenStringDoubleQuoted) {
$output[] = $token;
}
elseif ($token instanceof TokenNumber) {
} elseif ($token instanceof TokenStringSingleQuoted) {
$output[] = $token;
}
elseif ($token instanceof TokenVariable) {
} elseif ($token instanceof TokenNumber) {
$output[] = $token;
}
elseif ($token instanceof TokenFunction) {
} elseif ($token instanceof TokenVariable) {
$output[] = $token;
} elseif ($token instanceof TokenFunction) {
array_push($stack, $token);
}
elseif ($token instanceof AbstractOperator) {
} 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)
&& (
// 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())
)
)
)
||
// 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)) ) {
// And not a left bracket
&& (!($stack[$count - 1] instanceof TokenLeftBracket))) {
$output[] = array_pop($stack);
}
array_push($stack, $token);
}
elseif ($token instanceof TokenLeftBracket) {
} elseif ($token instanceof TokenLeftBracket) {
array_push($stack, $token);
}
elseif ($token instanceof TokenRightBracket) {
while (($current = array_pop($stack)) && ( ! ($current instanceof TokenLeftBracket))) {
} elseif ($token instanceof TokenRightBracket) {
while (($current = array_pop($stack)) && (!($current instanceof TokenLeftBracket))) {
$output[] = $current;
}
if (!empty($stack) && ($stack[count($stack)-1] instanceof TokenFunction)) {
if (!empty($stack) && ($stack[count($stack) - 1] instanceof TokenFunction)) {
$output[] = array_pop($stack);
}
}

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
abstract class AbstractContainerToken implements InterfaceToken
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
abstract class AbstractOperator implements InterfaceToken, InterfaceOperator
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
interface InterfaceFunction
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
interface InterfaceOperator
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
interface InterfaceToken
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenComma implements InterfaceToken
{

View file

@ -13,7 +13,7 @@ namespace NXP\Classes\Token;
use NXP\Exception\IncorrectExpressionException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenDegree extends AbstractOperator
{

View file

@ -14,7 +14,7 @@ use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\DivisionByZeroException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenDivision extends AbstractOperator
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenFunction extends AbstractContainerToken implements InterfaceFunction
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenLeftBracket implements InterfaceToken
{

View file

@ -13,7 +13,7 @@ namespace NXP\Classes\Token;
use NXP\Exception\IncorrectExpressionException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenMinus extends AbstractOperator
{

View file

@ -13,7 +13,7 @@ namespace NXP\Classes\Token;
use NXP\Exception\IncorrectExpressionException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenMultiply extends AbstractOperator
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenNumber extends AbstractContainerToken
{

View file

@ -13,7 +13,7 @@ namespace NXP\Classes\Token;
use NXP\Exception\IncorrectExpressionException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenPlus extends AbstractOperator
{

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenRightBracket implements InterfaceToken
{

View file

@ -13,7 +13,7 @@ namespace NXP\Classes\Token;
/**
* @author Bruce Wells <brucekwells@gmail.com>
*/
class TokenString extends AbstractContainerToken
class TokenStringDoubleQuoted extends AbstractContainerToken
{
/**
* @return string

View file

@ -0,0 +1,26 @@
<?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 "'([^']|'')*'";
}
}

View file

@ -11,7 +11,7 @@
namespace NXP\Classes\Token;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenVariable extends AbstractContainerToken
{

View file

@ -16,14 +16,15 @@ 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\TokenString;
use NXP\Classes\Token\TokenStringDoubleQuoted;
use NXP\Exception\UnknownFunctionException;
use NXP\Exception\UnknownOperatorException;
use NXP\Exception\UnknownTokenException;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class TokenFactory
{
@ -50,12 +51,17 @@ class TokenFactory
/**
* Add function
* @param string $name
* @param string $name
* @param callable $function
* @param int $places
* @param int $places
* @throws \ReflectionException
*/
public function addFunction($name, callable $function, $places = 1)
public function addFunction($name, callable $function, $places = null)
{
if ($places === null) {
$reflector = new \ReflectionFunction($function);
$places = $reflector->getNumberOfParameters();
}
$this->functions[$name] = [$places, $function];
}
@ -72,8 +78,9 @@ class TokenFactory
/**
* Add operator
* @param string $operatorClass
* @throws \NXP\Exception\UnknownOperatorException
* @param string $operatorClass
* @throws UnknownOperatorException
* @throws \ReflectionException
*/
public function addOperator($operatorClass)
{
@ -83,8 +90,7 @@ class TokenFactory
throw new UnknownOperatorException($operatorClass);
}
$this->operators[] = $operatorClass;
$this->operators = array_unique($this->operators);
$this->operators[$operatorClass::getRegex()] = $operatorClass;
}
/**
@ -131,9 +137,10 @@ class TokenFactory
}
return sprintf(
'/(%s)|(%s)|([%s])|(%s)|(%s)|([%s%s%s])/i',
'/(%s)|(%s)|(%s)|([%s])|(%s)|(%s)|([%s%s%s])/i',
TokenNumber::getRegex(),
TokenString::getRegex(),
TokenStringDoubleQuoted::getRegex(),
TokenStringSingleQuoted::getRegex(),
$operatorsRegex,
TokenFunction::getRegex(),
TokenVariable::getRegex(),
@ -144,9 +151,10 @@ class TokenFactory
}
/**
* @param string $token
* @param string $token
* @return InterfaceToken
* @throws UnknownTokenException
* @throws UnknownFunctionException
*/
public function createToken($token)
{
@ -163,7 +171,11 @@ class TokenFactory
}
if ($token[0] == '"') {
return new TokenString(str_replace('"', '', $token));
return new TokenStringDoubleQuoted(str_replace('"', '', $token));
}
if ($token[0] == "'") {
return new TokenStringSingleQuoted(str_replace("'", '', $token));
}
if ($token == ',') {
@ -180,7 +192,7 @@ class TokenFactory
$regex = sprintf('/%s/i', TokenVariable::getRegex());
if (preg_match($regex, $token)) {
return new TokenVariable(substr($token,1));
return new TokenVariable(substr($token, 1));
}
$regex = sprintf('/%s/i', TokenFunction::getRegex());

View file

@ -12,7 +12,7 @@
namespace NXP\Exception;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class IncorrectBracketsException extends MathExecutorException
{

View file

@ -12,7 +12,7 @@
namespace NXP\Exception;
/**
* @author Alexander Kiryukhin <alexander@symdev.org>
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class UnknownVariableException extends MathExecutorException
{

View file

@ -13,7 +13,6 @@ namespace NXP;
use NXP\Classes\Calculator;
use NXP\Classes\Lexer;
use NXP\Classes\Token;
use NXP\Classes\TokenFactory;
use NXP\Exception\UnknownVariableException;
@ -53,35 +52,6 @@ class MathExecutor
$this->addDefaults();
}
/**
* Set default operands and functions
*/
protected function addDefaults()
{
$this->tokenFactory = new TokenFactory();
$this->tokenFactory->addOperator('NXP\Classes\Token\TokenPlus');
$this->tokenFactory->addOperator('NXP\Classes\Token\TokenMinus');
$this->tokenFactory->addOperator('NXP\Classes\Token\TokenMultiply');
$this->tokenFactory->addOperator('NXP\Classes\Token\TokenDivision');
$this->tokenFactory->addOperator('NXP\Classes\Token\TokenDegree');
$this->tokenFactory->addFunction('sin', 'sin');
$this->tokenFactory->addFunction('cos', 'cos');
$this->tokenFactory->addFunction('tn', 'tan');
$this->tokenFactory->addFunction('asin', 'asin');
$this->tokenFactory->addFunction('acos', 'acos');
$this->tokenFactory->addFunction('atn', 'atan');
$this->tokenFactory->addFunction('min', 'min', 2);
$this->tokenFactory->addFunction('max', 'max', 2);
$this->tokenFactory->addFunction('avg', function($arg1, $arg2) { return ($arg1 + $arg2) / 2; }, 2);
$this->setVars([
'pi' => 3.14159265359,
'e' => 2.71828182846
]);
}
/**
* Get all vars
*
@ -95,13 +65,13 @@ class MathExecutor
/**
* Get a specific var
*
* @param string $variable
* @param string $variable
* @return integer|float
* @throws UnknownVariableException
*/
public function getVar($variable)
{
if (! isset($this->variables[$variable])) {
if (!isset($this->variables[$variable])) {
throw new UnknownVariableException("Variable ({$variable}) not set");
}
@ -111,9 +81,10 @@ class MathExecutor
/**
* Add variable to executor
*
* @param string $variable
* @param string $variable
* @param integer|float $value
* @return MathExecutor
* @throws \Exception
*/
public function setVar($variable, $value)
{
@ -129,9 +100,10 @@ class MathExecutor
/**
* Add variables to executor
*
* @param array $variables
* @param bool $clear Clear previous variables
* @param array $variables
* @param bool $clear Clear previous variables
* @return MathExecutor
* @throws \Exception
*/
public function setVars(array $variables, $clear = true)
{
@ -149,7 +121,7 @@ class MathExecutor
/**
* Remove variable from executor
*
* @param string $variable
* @param string $variable
* @return MathExecutor
*/
public function removeVar($variable)
@ -172,8 +144,9 @@ class MathExecutor
/**
* Add operator to executor
*
* @param string $operatorClass Class of operator token
* @param string $operatorClass Class of operator token
* @return MathExecutor
* @throws Exception\UnknownOperatorException
*/
public function addOperator($operatorClass)
{
@ -195,10 +168,11 @@ class MathExecutor
/**
* Add function to executor
*
* @param string $name Name of function
* @param callable $function Function
* @param int $places Count of arguments
* @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 = 1)
{
@ -262,4 +236,74 @@ class MathExecutor
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());
}
protected function defaultOperators()
{
return [
'NXP\Classes\Token\TokenPlus',
'NXP\Classes\Token\TokenMinus',
'NXP\Classes\Token\TokenMultiply',
'NXP\Classes\Token\TokenDivision',
'NXP\Classes\Token\TokenDegree',
];
}
protected function defaultFunctions()
{
return [
'sin' => function ($arg) {
return sin($arg);
},
'cos' => function ($arg) {
return cos($arg);
},
'tn' => function ($arg) {
return tan($arg);
},
'asin' => function ($arg) {
return asin($arg);
},
'acos' => function ($arg) {
return acos($arg);
},
'atn' => function ($arg) {
return atan($arg);
},
'min' => function ($arg1, $arg2) {
return min($arg1, $arg2);
},
'max' => function ($arg1, $arg2) {
return max($arg1, $arg2);
},
'avg' => function ($arg1, $arg2) {
return ($arg1 + $arg2) / 2;
},
];
}
protected function defaultVars()
{
return [
'pi' => 3.14159265359,
'e' => 2.71828182846
];
}
}

View file

@ -32,7 +32,7 @@ class MathTest extends \PHPUnit_Framework_TestCase
/** @var float $phpResult */
eval('$phpResult = ' . $expression . ';');
$this->assertEquals($calculator->execute($expression), $phpResult);
$this->assertEquals($calculator->execute($expression), $phpResult, "Expression was: ${expression}");
}
/**
@ -138,9 +138,23 @@ class MathTest extends \PHPUnit_Framework_TestCase
{
$calculator = new MathExecutor();
$calculator->addFunction('round', function ($arg) { return round($arg); }, 1);
$calculator->addFunction('round', function ($arg) {
return round($arg);
}, 1);
/** @var float $phpResult */
eval('$phpResult = round(100/30);');
$this->assertEquals($calculator->execute('round(100/30)'), $phpResult);
}
public function testQuotes()
{
$calculator = new MathExecutor();
$testString = "some, long. arg; with: different-separators!";
$calculator->addFunction('test', function ($arg) use ($testString) {
$this->assertEquals($arg, $testString);
return 0;
}, 1);
$calculator->execute('test("' . $testString . '")'); // single quotes
$calculator->execute("test('" . $testString . "')"); // double quotes
}
}