aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/tests.yml46
-rw-r--r--README.md18
-rw-r--r--composer.json13
-rw-r--r--src/NXP/Classes/Tokenizer.php70
-rw-r--r--src/NXP/MathExecutor.php138
-rw-r--r--tests/MathTest.php118
6 files changed, 329 insertions, 74 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 557134d..1ef4950 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,19 +1,39 @@
name: Tests
-on: [push]
+on: [push, pull_request]
jobs:
- build-test:
- runs-on: ubuntu-latest
+ php-tests:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ php: [8.0, 7.4, 7.3, 7.2]
+ dependency-version: [prefer-lowest, prefer-stable]
+ os: [ubuntu-latest, windows-latest]
+
+ name: ${{ matrix.os }} - PHP${{ matrix.php }} - ${{ matrix.dependency-version }}
steps:
- - uses: actions/checkout@v1
- - uses: php-actions/composer@v1
- - name: PHPUnit
- uses: php-actions/phpunit@v1
- with:
- config: ./phpunit.xml.dist
- - name: PHP CS Fixer
- uses: StephaneBour/actions-php-cs-fixer@1.0
- with:
- dir: './src'
+ - name: Checkout code
+ uses: actions/checkout@v1
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip, bcmath, intl
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer install --no-interaction
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
+
+ - name: Execute tests
+ run: vendor/bin/phpunit
+
+ - name: PHP CS Fixer
+ uses: StephaneBour/actions-php-cs-fixer@1.0
+ if: matrix.os != 'windows-latest'
+ with:
+ dir: './src' \ No newline at end of file
diff --git a/README.md b/README.md
index f88e5e3..f589116 100644
--- a/README.md
+++ b/README.md
@@ -34,17 +34,22 @@ echo $executor->execute('1 + 2 * (2 - (4+10))^2 + sin(10)');
## Functions:
Default functions:
* abs
-* acos
+* acos (arccos)
* acosh
-* asin
-* atan (atn)
+* arcctg (arccot, arccotan)
+* arcsec
+* arccsc (arccosec)
+* asin (arcsin)
+* atan (atn, arctan, arctg)
* atan2
* atanh
* avg
* bindec
* ceil
* cos
+* cosec (csc)
* cosh
+* ctg (cot, cotan, cotg, ctn)
* decbin
* dechex
* decoct
@@ -57,8 +62,8 @@ Default functions:
* hypot
* if
* intdiv
-* log
-* log10
+* log (ln)
+* log10 (lg)
* log1p
* max
* min
@@ -67,10 +72,11 @@ Default functions:
* pow
* rad2deg
* round
+* sec
* sin
* sinh
* sqrt
-* tan (tn)
+* tan (tn, tg)
* tanh
Add custom function to executor:
diff --git a/composer.json b/composer.json
index 3ba5a22..6c2dd75 100644
--- a/composer.json
+++ b/composer.json
@@ -23,14 +23,19 @@
}
],
"require": {
- "php": ">=7.1"
+ "php": ">=7.2"
},
"require-dev": {
- "phpunit/phpunit": "~8.0"
+ "phpunit/phpunit": ">=8.0"
},
"autoload": {
- "psr-0": {
- "NXP": "src/"
+ "psr-4": {
+ "NXP\\": "src/NXP"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "NXP\\Tests\\": "tests/"
}
}
}
diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php
index 203724a..f8056c3 100644
--- a/src/NXP/Classes/Tokenizer.php
+++ b/src/NXP/Classes/Tokenizer.php
@@ -27,15 +27,15 @@ class Tokenizer
/**
* @var string
*/
- private $input = "";
+ private $input = '';
/**
* @var string
*/
- private $numberBuffer = "";
+ private $numberBuffer = '';
/**
* @var string
*/
- private $stringBuffer = "";
+ private $stringBuffer = '';
/**
* @var bool
*/
@@ -74,25 +74,25 @@ class Tokenizer
if ($ch === "'") {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inSingleQuotedString = false;
- $this->stringBuffer = "";
+ $this->stringBuffer = '';
continue 2;
}
$this->stringBuffer .= $ch;
continue 2;
case $this->inDoubleQuotedString:
- if ($ch === "\"") {
+ if ($ch === '"') {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inDoubleQuotedString = false;
- $this->stringBuffer = "";
+ $this->stringBuffer = '';
continue 2;
}
$this->stringBuffer .= $ch;
continue 2;
- case $ch == " " || $ch == "\n" || $ch == "\r" || $ch == "\t":
- $this->tokens[] = new Token(Token::Space, "");
+ case $ch == ' ' || $ch == "\n" || $ch == "\r" || $ch == "\t":
+ $this->tokens[] = new Token(Token::Space, '');
continue 2;
case $this->isNumber($ch):
- if ($this->stringBuffer != "") {
+ if ($this->stringBuffer != '') {
$this->stringBuffer .= $ch;
continue 2;
}
@@ -100,22 +100,22 @@ class Tokenizer
$this->allowNegative = false;
break;
/** @noinspection PhpMissingBreakStatementInspection */
- case strtolower($ch) === "e":
- if ($this->numberBuffer != "" && strpos($this->numberBuffer, ".") !== false) {
- $this->numberBuffer .= "e";
- $this->allowNegative = true;
+ case strtolower($ch) === 'e':
+ if (strlen($this->numberBuffer) && strpos($this->numberBuffer, '.') !== false) {
+ $this->numberBuffer .= 'e';
+ $this->allowNegative = false;
break;
}
// no break
case $this->isAlpha($ch):
- if ($this->numberBuffer != "") {
+ if (strlen($this->numberBuffer)) {
$this->emptyNumberBufferAsLiteral();
- $this->tokens[] = new Token(Token::Operator, "*");
+ $this->tokens[] = new Token(Token::Operator, '*');
}
$this->allowNegative = false;
$this->stringBuffer .= $ch;
break;
- case $ch == "\"":
+ case $ch == '"':
$this->inDoubleQuotedString = true;
continue 2;
case $ch == "'":
@@ -127,33 +127,41 @@ class Tokenizer
$this->allowNegative = false;
break;
case $this->isLP($ch):
- if ($this->stringBuffer != "") {
+ if ($this->stringBuffer != '') {
$this->tokens[] = new Token(Token::Function, $this->stringBuffer);
- $this->stringBuffer = "";
- } elseif ($this->numberBuffer != "") {
+ $this->stringBuffer = '';
+ } elseif (strlen($this->numberBuffer)) {
$this->emptyNumberBufferAsLiteral();
- $this->tokens[] = new Token(Token::Operator, "*");
+ $this->tokens[] = new Token(Token::Operator, '*');
}
$this->allowNegative = true;
- $this->tokens[] = new Token(Token::LeftParenthesis, "");
+ $this->tokens[] = new Token(Token::LeftParenthesis, '');
break;
case $this->isRP($ch):
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
$this->allowNegative = false;
- $this->tokens[] = new Token(Token::RightParenthesis, "");
+ $this->tokens[] = new Token(Token::RightParenthesis, '');
break;
case $this->isComma($ch):
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
$this->allowNegative = true;
- $this->tokens[] = new Token(Token::ParamSeparator, "");
+ $this->tokens[] = new Token(Token::ParamSeparator, '');
break;
default:
- if ($this->allowNegative && $ch == "-") {
- $this->allowNegative = false;
- $this->numberBuffer .= "-";
- continue 2;
+ // special case for unary operations
+ if ($ch == '-' || $ch == '+') {
+ if ($this->allowNegative) {
+ $this->allowNegative = false;
+ $this->tokens[] = new Token(Token::Operator, $ch == '-' ? 'uNeg' : 'uPos');
+ continue 2;
+ }
+ // could be in exponent, in which case negative should be added to the numberBuffer
+ if ($this->numberBuffer && $this->numberBuffer[strlen($this->numberBuffer) - 1] == 'e') {
+ $this->numberBuffer .= '-';
+ continue 2;
+ }
}
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
@@ -188,9 +196,9 @@ class Tokenizer
private function emptyNumberBufferAsLiteral() : void
{
- if ($this->numberBuffer != "") {
+ if (strlen($this->numberBuffer)) {
$this->tokens[] = new Token(Token::Literal, $this->numberBuffer);
- $this->numberBuffer = "";
+ $this->numberBuffer = '';
}
}
@@ -211,9 +219,9 @@ class Tokenizer
private function emptyStrBufferAsVariable() : void
{
- if ($this->stringBuffer != "") {
+ if ($this->stringBuffer != '') {
$this->tokens[] = new Token(Token::Variable, $this->stringBuffer);
- $this->stringBuffer = "";
+ $this->stringBuffer = '';
}
}
diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php
index b7abcad..a774561 100644
--- a/src/NXP/MathExecutor.php
+++ b/src/NXP/MathExecutor.php
@@ -16,8 +16,8 @@ use NXP\Classes\CustomFunction;
use NXP\Classes\Operator;
use NXP\Classes\Tokenizer;
use NXP\Exception\DivisionByZeroException;
-use NXP\Exception\UnknownVariableException;
use NXP\Exception\MathExecutorException;
+use NXP\Exception\UnknownVariableException;
use ReflectionException;
/**
@@ -85,28 +85,42 @@ class MathExecutor
protected function defaultOperators() : array
{
return [
- '+' => [
+ '+' => [
function ($a, $b) {
return $a + $b;
},
170,
false
],
- '-' => [
+ '-' => [
function ($a, $b) {
return $a - $b;
},
170,
false
],
- '*' => [
+ 'uPos' => [ // unary positive token
+ function ($a) {
+ return $a;
+ },
+ 200,
+ false
+ ],
+ 'uNeg' => [ // unary minus token
+ function ($a) {
+ return 0 - $a;
+ },
+ 200,
+ false
+ ],
+ '*' => [
function ($a, $b) {
return $a * $b;
},
180,
false
],
- '/' => [
+ '/' => [
function ($a, $b) {
if ($b == 0) {
throw new DivisionByZeroException();
@@ -116,28 +130,28 @@ class MathExecutor
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) {
if (is_string($a) || is_string($b)) {
return strcmp($a, $b) == 0;
@@ -148,7 +162,7 @@ class MathExecutor
140,
false
],
- '!=' => [
+ '!=' => [
function ($a, $b) {
if (is_string($a) || is_string($b)) {
return strcmp($a, $b) != 0;
@@ -159,28 +173,28 @@ class MathExecutor
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;
},
@@ -220,6 +234,36 @@ class MathExecutor
'acosh' => function ($arg) {
return acosh($arg);
},
+ 'arcsin' => function ($arg) {
+ return asin($arg);
+ },
+ 'arcctg' => function ($arg) {
+ return M_PI/2 - atan($arg);
+ },
+ 'arccot' => function ($arg) {
+ return M_PI/2 - atan($arg);
+ },
+ 'arccotan' => function ($arg) {
+ return M_PI/2 - atan($arg);
+ },
+ 'arcsec' => function ($arg) {
+ return acos(1/$arg);
+ },
+ 'arccosec' => function ($arg) {
+ return asin(1/$arg);
+ },
+ 'arccsc' => function ($arg) {
+ return asin(1/$arg);
+ },
+ 'arccos' => function ($arg) {
+ return acos($arg);
+ },
+ 'arctan' => function ($arg) {
+ return atan($arg);
+ },
+ 'arctg' => function ($arg) {
+ return atan($arg);
+ },
'asin' => function ($arg) {
return asin($arg);
},
@@ -247,9 +291,30 @@ class MathExecutor
'cos' => function ($arg) {
return cos($arg);
},
+ 'cosec' => function ($arg) {
+ return 1 / sin($arg);
+ },
+ 'csc' => function ($arg) {
+ return 1 / sin($arg);
+ },
'cosh' => function ($arg) {
return cosh($arg);
},
+ 'ctg' => function ($arg) {
+ return cos($arg) / sin($arg);
+ },
+ 'cot' => function ($arg) {
+ return cos($arg) / sin($arg);
+ },
+ 'cotan' => function ($arg) {
+ return cos($arg) / sin($arg);
+ },
+ 'cotg' => function ($arg) {
+ return cos($arg) / sin($arg);
+ },
+ 'ctn' => function ($arg) {
+ return cos($arg) / sin($arg);
+ },
'decbin' => function ($arg) {
return decbin($arg);
},
@@ -295,6 +360,12 @@ class MathExecutor
'intdiv' => function ($arg1, $arg2) {
return intdiv($arg1, $arg2);
},
+ 'ln' => function ($arg) {
+ return log($arg);
+ },
+ 'lg' => function ($arg) {
+ return log10($arg);
+ },
'log' => function ($arg) {
return log($arg);
},
@@ -317,7 +388,7 @@ class MathExecutor
return pi();
},
'pow' => function ($arg1, $arg2) {
- return pow($arg1, $arg2);
+ return $arg1 ** $arg2;
},
'rad2deg' => function ($arg) {
return rad2deg($arg);
@@ -331,6 +402,9 @@ class MathExecutor
'sinh' => function ($arg) {
return sinh($arg);
},
+ 'sec' => function ($arg) {
+ return 1 / cos($arg);
+ },
'sqrt' => function ($arg) {
return sqrt($arg);
},
@@ -342,6 +416,9 @@ class MathExecutor
},
'tn' => function ($arg) {
return tan($arg);
+ },
+ 'tg' => function ($arg) {
+ return tan($arg);
}
];
}
@@ -349,22 +426,26 @@ class MathExecutor
/**
* Execute expression
*
- * @param $expression
+ * @param string $expression
+ * @param bool $cache
* @return number
- * @throws Exception\IncorrectExpressionException
* @throws Exception\IncorrectBracketsException
+ * @throws Exception\IncorrectExpressionException
* @throws Exception\UnknownOperatorException
- * @throws Exception\UnknownVariableException
+ * @throws UnknownVariableException
*/
- public function execute(string $expression)
+ public function execute(string $expression, bool $cache = true)
{
$cachekey = $expression;
if (!array_key_exists($cachekey, $this->cache)) {
$tokens = (new Tokenizer($expression, $this->operators))->tokenize()->buildReversePolishNotation();
- $this->cache[$cachekey] = $tokens;
+ if ($cache) {
+ $this->cache[$cachekey] = $tokens;
+ }
} else {
$tokens = $this->cache[$cachekey];
}
+
$calculator = new Calculator($this->functions, $this->operators);
return $calculator->calculate($tokens, $this->variables, $this->onVarNotFound);
}
@@ -533,6 +614,23 @@ class MathExecutor
return $this;
}
+ /**
+ * Get cache array with tokens
+ * @return array
+ */
+ public function getCache() : array
+ {
+ return $this->cache;
+ }
+
+ /**
+ * Clear token's cache
+ */
+ public function clearCache() : void
+ {
+ $this->cache = [];
+ }
+
public function __clone()
{
$this->addDefaults();
diff --git a/tests/MathTest.php b/tests/MathTest.php
index 4ea0cb0..259de2c 100644
--- a/tests/MathTest.php
+++ b/tests/MathTest.php
@@ -147,6 +147,8 @@ class MathTest extends TestCase
['sin(10) * cos(50) / min(10, (20/2))'],
['sin(10) * cos(50) / min(10, (max(10,20)/2))'],
+ ['100500 * 3.5e5'],
+ ['100500 * 3.5e-5'],
['100500 * 3.5E5'],
['100500 * 3.5E-5'],
@@ -208,6 +210,24 @@ class MathTest extends TestCase
['1 + (3 *-1)'],
['1 - 0'],
['1-0'],
+
+ ['-(1.5)'],
+ ['-log(4)'],
+ ['0-acosh(1.5)'],
+ ['-acosh(1.5)'],
+ ['-(-4)'],
+ ['-(-4 + 5)'],
+ ['-(3 * 1)'],
+ ['-(-3 * -1)'],
+ ['-1 + (-3 * -1)'],
+ ['-1 + ( -3 * 1)'],
+ ['-1 + (3 *-1)'],
+ ['-1 - 0'],
+ ['-1-0'],
+ ['-(4*2)-5'],
+ ['-(4*-2)-5'],
+ ['-(-4*2) - 5'],
+ ['-4*-5'],
];
}
@@ -232,6 +252,17 @@ class MathTest extends TestCase
$this->assertEquals(0, $calculator->execute('10 / 0'));
}
+ public function testUnaryOperators()
+ {
+ $calculator = new MathExecutor();
+ $this->assertEquals(5, $calculator->execute('+5'));
+ $this->assertEquals(5, $calculator->execute('+(3+2)'));
+ $this->assertEquals(-5, $calculator->execute('-5'));
+ $this->assertEquals(5, $calculator->execute('-(-5)'));
+ $this->assertEquals(-5, $calculator->execute('+(-5)'));
+ $this->assertEquals(-5, $calculator->execute('-(3+2)'));
+ }
+
public function testZeroDivisionException()
{
$calculator = new MathExecutor();
@@ -526,4 +557,91 @@ class MathTest extends TestCase
$this->expectException(MathExecutorException::class);
$calculator->setVar('resource', tmpfile());
}
+
+ /**
+ * @dataProvider providerExpressionValues
+ */
+ public function testCalculatingValues($expression, $value)
+ {
+ $calculator = new MathExecutor();
+
+ 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($value, $result, "${expression} did not evaluate to {$value}");
+ }
+
+ /**
+ * Expressions data provider
+ *
+ * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP directly.
+ * 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 complex and not a simple mathmatical expression.
+ */
+ public function providerExpressionValues()
+ {
+ return [
+ ['arccos(0.5)', 1.0471975511966],
+ ['arccos(0.5)', acos(0.5)],
+ ['arccosec(4)', 0.2526802551421],
+ ['arccosec(4)', asin(1/4)],
+ ['arccot(3)', M_PI/2 - atan(3)],
+ ['arccotan(4)', 0.2449786631269],
+ ['arccotan(4)', M_PI/2 - atan(4)],
+ ['arccsc(4)', 0.2526802551421],
+ ['arccsc(4)', asin(1/4)],
+ ['arcctg(3)', M_PI/2 - atan(3)],
+ ['arcsec(4)', 1.3181160716528],
+ ['arcsec(4)', acos(1/4)],
+ ['arcsin(0.5)', 0.5235987755983],
+ ['arcsin(0.5)', asin(0.5)],
+ ['arctan(0.5)', atan(0.5)],
+ ['arctan(4)', 1.3258176636680],
+ ['arctg(0.5)', atan(0.5)],
+ ['cosec(12)', 1 / sin(12)],
+ ['cosec(4)', -1.3213487088109],
+ ['cosh(12)', cosh(12)],
+ ['cot(12)', cos(12) / sin(12)],
+ ['cotan(12)', cos(12) / sin(12)],
+ ['cotan(4)', 0.8636911544506],
+ ['cotg(3)', cos(3) / sin(3)],
+ ['csc(4)', 1 / sin(4)],
+ ['ctg(4)', cos(4) / sin(4)],
+ ['ctn(4)', cos(4) / sin(4)],
+ ['decbin(10)', decbin(10)],
+ ['lg(2)', 0.3010299956639],
+ ['lg(2)', log10(2)],
+ ['ln(2)', 0.6931471805599],
+ ['ln(2)', log(2)],
+ ['sec(4)', -1.5298856564664],
+ ['tg(4)', 1.1578212823496],
+ ];
+ }
+
+ public function testCache()
+ {
+ $calculator = new MathExecutor();
+ $this->assertEquals(256, $calculator->execute('2 ^ 8')); // second arg $cache is true by default
+
+ $this->assertIsArray($calculator->getCache());
+ $this->assertEquals(1, count($calculator->getCache()));
+
+ $this->assertEquals(512, $calculator->execute('2 ^ 9', true));
+ $this->assertEquals(2, count($calculator->getCache()));
+
+ $this->assertEquals(1024, $calculator->execute('2 ^ 10', false));
+ $this->assertEquals(2, count($calculator->getCache()));
+
+ $calculator->clearCache();
+ $this->assertIsArray($calculator->getCache());
+ $this->assertEquals(0, count($calculator->getCache()));
+
+ $this->assertEquals(2048, $calculator->execute('2 ^ 11', false));
+ $this->assertEquals(0, count($calculator->getCache()));
+
+
+ }
+
} \ No newline at end of file