diff options
-rw-r--r-- | .github/workflows/tests.yml | 46 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | composer.json | 13 | ||||
-rw-r--r-- | src/NXP/Classes/Tokenizer.php | 70 | ||||
-rw-r--r-- | src/NXP/MathExecutor.php | 138 | ||||
-rw-r--r-- | tests/MathTest.php | 118 |
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 @@ -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 |