diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4967599..2db0a46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.2, 8.1, 8.0, 7.4] + php: [8.2, 8.1, 8.0] dependency-version: [prefer-lowest, prefer-stable] os: [ubuntu-latest, windows-latest] diff --git a/README.md b/README.md index 0e88f34..b84aace 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# MathExecutor [![Tests](https://github.com/neonxp/MathExecutor/workflows/Tests/badge.svg)](https://github.com/neonxp/MathExecutor/actions?query=workflow%3ATests) +# MathExecutor [![Tests](https://github.com/neonxp/MathExecutor/workflows/Tests/badge.svg)](https://github.com/neonxp/MathExecutor/actions?query=workflow%3ATests) ![](https://img.shields.io/badge/PHPStan-level%206-brightgreen.svg?style=flat) # A simple and extensible math expressions calculator diff --git a/composer.json b/composer.json index 6d3759d..1defb99 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ }, "require-dev": { "phpunit/phpunit": ">=9.0", - "friendsofphp/php-cs-fixer": "^3.8" + "friendsofphp/php-cs-fixer": "^3.8", + "phpstan/phpstan": "^1.9" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index bed17ae..0000000 --- a/phpstan.neon +++ /dev/null @@ -1,9 +0,0 @@ -parameters: - level: 6 - paths: - - ./ - excludes_analyse: - - vendor/* - - tests/* - bootstrapFiles: - - vendor/autoload.php \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b05dc95 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: 6 + errorFormat: raw + editorUrl: '%%file%% %%line%% %%column%%: %%error%%' + paths: + - src + - tests diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 6b63503..80dee36 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -20,22 +20,14 @@ use NXP\Exception\UnknownVariableException; */ class Calculator { - /** @var array */ - private array $functions = []; - - /** @var array */ - private array $operators = []; - /** * @todo PHP8: Use constructor property promotion -> public function __construct(private array $functions, private array $operators) * * @param array $functions * @param array $operators */ - public function __construct(array $functions, array $operators) + public function __construct(private array $functions, private array $operators) { - $this->functions = $functions; - $this->operators = $operators; } /** diff --git a/src/NXP/Classes/CustomFunction.php b/src/NXP/Classes/CustomFunction.php index 85db379..e4cdd76 100644 --- a/src/NXP/Classes/CustomFunction.php +++ b/src/NXP/Classes/CustomFunction.php @@ -8,8 +8,6 @@ use ReflectionFunction; class CustomFunction { - public string $name = ''; - /** * @var callable $function */ @@ -26,9 +24,8 @@ class CustomFunction * * @throws ReflectionException */ - public function __construct(string $name, callable $function) + public function __construct(public string $name, callable $function) { - $this->name = $name; $this->function = $function; $reflection = (new ReflectionFunction($function)); $this->isVariadic = $reflection->isVariadic(); diff --git a/src/NXP/Classes/Operator.php b/src/NXP/Classes/Operator.php index 7dee06d..231b1d9 100644 --- a/src/NXP/Classes/Operator.php +++ b/src/NXP/Classes/Operator.php @@ -7,12 +7,6 @@ use ReflectionFunction; class Operator { - public string $operator = ''; - - public bool $isRightAssoc = false; - - public int $priority = 0; - /** * @var callable(\SplStack) */ @@ -23,11 +17,8 @@ class Operator /** * Operator constructor. */ - public function __construct(string $operator, bool $isRightAssoc, int $priority, callable $function) + public function __construct(public string $operator, public bool $isRightAssoc, public int $priority, callable $function) { - $this->operator = $operator; - $this->isRightAssoc = $isRightAssoc; - $this->priority = $priority; $this->function = $function; $reflection = new ReflectionFunction($function); $this->places = $reflection->getNumberOfParameters(); diff --git a/src/NXP/Classes/Token.php b/src/NXP/Classes/Token.php index 7532e77..ae460a9 100644 --- a/src/NXP/Classes/Token.php +++ b/src/NXP/Classes/Token.php @@ -22,22 +22,13 @@ class Token public const Space = 'space'; - public string $type = self::Literal; - - public $value; - - public ?string $name; - public ?int $paramCount = null;//to store function parameter count in stack /** * Token constructor. * */ - public function __construct(string $type, $value, ?string $name = null) + public function __construct(public string $type, public mixed $value, public ?string $name = null) { - $this->type = $type; - $this->value = $value; - $this->name = $name; } } diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php index a74bff6..656961a 100644 --- a/src/NXP/Classes/Tokenizer.php +++ b/src/NXP/Classes/Tokenizer.php @@ -23,17 +23,12 @@ class Tokenizer /** @var array */ public array $tokens = []; - private string $input = ''; - private string $numberBuffer = ''; private string $stringBuffer = ''; private bool $allowNegative = true; - /** @var array */ - private array $operators = []; - private bool $inSingleQuotedString = false; private bool $inDoubleQuotedString = false; @@ -42,10 +37,8 @@ class Tokenizer * Tokenizer constructor. * @param Operator[] $operators */ - public function __construct(string $input, array $operators) + public function __construct(private string $input, private array $operators) { - $this->input = $input; - $this->operators = $operators; } public function tokenize() : self @@ -142,7 +135,7 @@ class Tokenizer break; /** @noinspection PhpMissingBreakStatementInspection */ case 'e' === \strtolower($ch): - if (\strlen($this->numberBuffer) && false !== \strpos($this->numberBuffer, '.')) { + if (\strlen($this->numberBuffer) && \str_contains($this->numberBuffer, '.')) { $this->numberBuffer .= 'e'; $this->allowNegative = false; @@ -330,7 +323,7 @@ class Tokenizer break; } $tokens[] = $ctoken; - } catch (RuntimeException $e) { + } catch (RuntimeException) { throw new IncorrectBracketsException(); } } diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index e991586..c857e99 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -140,9 +140,8 @@ class MathExecutor * Get a specific var * * @throws UnknownVariableException if VarNotFoundHandler is not set - * @return int|float */ - public function getVar(string $variable) + public function getVar(string $variable) : mixed { if (! \array_key_exists($variable, $this->variables)) { if ($this->onVarNotFound) { @@ -160,7 +159,7 @@ class MathExecutor * * @throws MathExecutorException if the value is invalid based on the default or custom validator */ - public function setVar(string $variable, $value) : self + public function setVar(string $variable, mixed $value) : self { if ($this->onVarValidation) { \call_user_func($this->onVarValidation, $variable, $value); @@ -273,8 +272,6 @@ class MathExecutor /** * Remove a specific operator - * - * @return array of operator class names */ public function removeOperator(string $operator) : self { @@ -380,7 +377,7 @@ class MathExecutor 180, false ], - '^' => [static fn ($a, $b) => \pow($a, $b), 220, true], + '^' => [static fn ($a, $b) => $a ** $b, 220, true], '%' => [static fn ($a, $b) => $a % $b, 180, false], '&&' => [static fn ($a, $b) => $a && $b, 100, false], '||' => [static fn ($a, $b) => $a || $b, 90, false], @@ -521,7 +518,7 @@ class MathExecutor * Default variable validation, ensures that the value is a scalar or array. * @throws MathExecutorException if the value is not a scalar */ - protected function defaultVarValidation(string $variable, $value) : void + protected function defaultVarValidation(string $variable, mixed $value) : void { if (! \is_scalar($value) && ! \is_array($value) && null !== $value) { $type = \gettype($value); diff --git a/tests/MathTest.php b/tests/MathTest.php index 0c87c45..2c9197f 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -31,12 +31,13 @@ class MathTest extends TestCase $calculator = new MathExecutor(); /** @var float $phpResult */ + $phpResult = 0.0; eval('$phpResult = ' . $expression . ';'); 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->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression)); } $this->assertEquals($phpResult, $result, "Expression was: {$expression}"); } @@ -47,6 +48,8 @@ class MathTest extends TestCase * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval. * 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. + * + * @return array> */ public function providerExpressions() { @@ -270,12 +273,13 @@ class MathTest extends TestCase } /** @var float $phpResult */ + $phpResult = 0.0; eval('$phpResult = ' . $expected . ';'); 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->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression)); } $this->assertEquals($phpResult, $result, "Expression was: {$expression}"); } @@ -286,6 +290,8 @@ class MathTest extends TestCase * Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval. * 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. + * + * @return array> */ public function bcMathExpressions() { @@ -505,6 +511,8 @@ class MathTest extends TestCase * Incorrect Expressions data provider * * These expressions should not pass validation + * + * @return array> */ public function incorrectExpressions() { @@ -592,9 +600,7 @@ class MathTest extends TestCase $this->assertEquals("'teststring", $calculator->execute("'\'teststring'")); $this->assertEquals("teststring'", $calculator->execute("'teststring\''")); - $calculator->addFunction('concat', static function($arg1, $arg2) { - return $arg1 . $arg2; - }); + $calculator->addFunction('concat', static fn ($arg1, $arg2) => $arg1 . $arg2); $this->assertEquals('test"ing', $calculator->execute('concat("test\"","ing")')); $this->assertEquals("test'ing", $calculator->execute("concat('test\'','ing')")); } @@ -608,7 +614,7 @@ class MathTest extends TestCase $this->assertEquals(\max([1, 5, 2]), $calculator->execute('max(array(1, 5, 2))')); $calculator->addFunction('arr_with_max_elements', static function($arg1, ...$args) { $args = \is_array($arg1) ? $arg1 : [$arg1, ...$args]; - \usort($args, static fn ($arr1, $arr2) => \count($arr2) <=> \count($arr1)); + \usort($args, static fn ($arr1, $arr2) => (\is_countable($arr2) ? \count($arr2) : 0) <=> \count($arr1)); return $args[0]; }); @@ -619,9 +625,7 @@ class MathTest extends TestCase { $calculator = new MathExecutor(); - $calculator->addFunction('concat', static function($arg1, $arg2) { - return $arg1 . $arg2; - }); + $calculator->addFunction('concat', static fn ($arg1, $arg2) => $arg1 . $arg2); $this->assertEquals('testing', $calculator->execute('concat("test","ing")')); $this->assertEquals('testing', $calculator->execute("concat('test','ing')")); } @@ -629,18 +633,14 @@ class MathTest extends TestCase public function testFunction() : void { $calculator = new MathExecutor(); - $calculator->addFunction('round', static function($arg) { - return \round($arg); - }); + $calculator->addFunction('round', static fn ($arg) => \round($arg)); $this->assertEquals(\round(100 / 30), $calculator->execute('round(100/30)')); } public function testFunctionUnlimitedParameters() : void { $calculator = new MathExecutor(); - $calculator->addFunction('give_me_an_array', static function() { - return [5, 3, 7, 9, 8]; - }); + $calculator->addFunction('give_me_an_array', static fn () => [5, 3, 7, 9, 8]); $this->assertEquals(6.4, $calculator->execute('avg(give_me_an_array())')); $this->assertEquals(10, $calculator->execute('avg(12,8,15,5)')); $this->assertEquals(3, $calculator->execute('min(give_me_an_array())')); @@ -657,9 +657,7 @@ class MathTest extends TestCase public function testFunctionOptionalParameters() : void { $calculator = new MathExecutor(); - $calculator->addFunction('round', static function($num, $precision = 0) { - return \round($num, $precision); - }); + $calculator->addFunction('round', static fn ($num, $precision = 0) => \round($num, $precision)); $this->assertEquals(\round(11.176), $calculator->execute('round(11.176)')); $this->assertEquals(\round(11.176, 2), $calculator->execute('round(11.176,2)')); } @@ -668,9 +666,7 @@ class MathTest extends TestCase { $calculator = new MathExecutor(); $this->expectException(IncorrectNumberOfFunctionParametersException::class); - $calculator->addFunction('myfunc', static function($arg1, $arg2) { - return $arg1 + $arg2; - }); + $calculator->addFunction('myfunc', static fn ($arg1, $arg2) => $arg1 + $arg2); $calculator->execute('myfunc(1)'); } @@ -678,9 +674,7 @@ class MathTest extends TestCase { $calculator = new MathExecutor(); $this->expectException(IncorrectNumberOfFunctionParametersException::class); - $calculator->addFunction('myfunc', static function($arg1, $arg2) { - return $arg1 + $arg2; - }); + $calculator->addFunction('myfunc', static fn ($arg1, $arg2) => $arg1 + $arg2); $calculator->execute('myfunc(1,2,3)'); } @@ -690,57 +684,57 @@ class MathTest extends TestCase $this->assertEquals( 30, $calculator->execute( - 'if(100 > 99, 30, 0)' - ), + 'if(100 > 99, 30, 0)' + ), 'Expression failed: if(100 > 99, 30, 0)' ); $this->assertEquals( 0, $calculator->execute( - 'if(100 < 99, 30, 0)' - ), + 'if(100 < 99, 30, 0)' + ), 'Expression failed: if(100 < 99, 30, 0)' ); $this->assertEquals( 30, $calculator->execute( - 'if(98 < 99 && sin(1) < 1, 30, 0)' - ), + 'if(98 < 99 && sin(1) < 1, 30, 0)' + ), 'Expression failed: if(98 < 99 && sin(1) < 1, 30, 0)' ); $this->assertEquals( 40, $calculator->execute( - 'if(98 < 99 && sin(1) < 1, max(30, 40), 0)' - ), + 'if(98 < 99 && sin(1) < 1, max(30, 40), 0)' + ), 'Expression failed: if(98 < 99 && sin(1) < 1, max(30, 40), 0)' ); $this->assertEquals( 40, $calculator->execute( - 'if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)' - ), + 'if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)' + ), 'Expression failed: if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)' ); $this->assertEquals( 20, $calculator->execute( - 'if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))' - ), + 'if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))' + ), 'Expression failed: if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))' ); $this->assertEquals( \cos(2), $calculator->execute( - 'if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))' - ), + 'if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))' + ), 'Expression failed: if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))' ); $this->assertEquals( \cos(2), $calculator->execute( - 'if(cos(2), cos(2), 0)' - ), + 'if(cos(2), cos(2), 0)' + ), 'Expression failed: if(cos(2), cos(2), 0)' ); $trx_amount = 100000; @@ -749,15 +743,15 @@ class MathTest extends TestCase $this->assertEquals( $trx_amount * 0.03, $calculator->execute( - 'if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)' - ), + 'if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)' + ), 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)' ); $this->assertEquals( $trx_amount * 0.03, $calculator->execute( - 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' - ), + 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' + ), 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' ); $trx_amount = 39000; @@ -765,8 +759,8 @@ class MathTest extends TestCase $this->assertEquals( $trx_amount * 0.06, $calculator->execute( - 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' - ), + 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' + ), 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' ); $trx_amount = 59000; @@ -774,16 +768,16 @@ class MathTest extends TestCase $this->assertEquals( $trx_amount * 0.05, $calculator->execute( - 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' - ), + 'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' + ), 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))' ); $this->expectException(IncorrectNumberOfFunctionParametersException::class); $this->assertEquals( 0.0, $calculator->execute( - 'if($trx_amount < 40000, $trx_amount * 0.06)' - ), + 'if($trx_amount < 40000, $trx_amount * 0.06)' + ), 'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06)' ); } @@ -832,9 +826,7 @@ class MathTest extends TestCase $calculator = new MathExecutor(); $calculator->addFunction( 'round', - static function($value, $decimals) { - return \round($value, $decimals); - } + static fn ($value, $decimals) => \round($value, $decimals) ); $expression = 'round(100 * 1.111111, 2)'; $phpResult = 0; @@ -848,9 +840,7 @@ class MathTest extends TestCase public function testFunctionsWithQuotes() : void { $calculator = new MathExecutor(); - $calculator->addFunction('concat', static function($first, $second) { - return $first . $second; - }); + $calculator->addFunction('concat', static fn ($first, $second) => $first . $second); $this->assertEquals('testing', $calculator->execute('concat("test", "ing")')); $this->assertEquals('testing', $calculator->execute("concat('test', 'ing')")); } @@ -1073,14 +1063,14 @@ class MathTest extends TestCase /** * @dataProvider providerExpressionValues */ - public function testCalculatingValues($expression, $value) : void + public function testCalculatingValues(string $expression, mixed $value) : void { $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->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression)); } $this->assertEquals($value, $result, "{$expression} did not evaluate to {$value}"); } @@ -1091,6 +1081,8 @@ class MathTest extends TestCase * 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. + * + * @return array> */ public function providerExpressionValues() {