diff options
-rw-r--r-- | src/NXP/Classes/Calculator.php | 2 | ||||
-rw-r--r-- | src/NXP/Classes/CustomFunction.php | 36 | ||||
-rw-r--r-- | src/NXP/Classes/Token.php | 2 | ||||
-rw-r--r-- | src/NXP/Classes/Tokenizer.php | 34 | ||||
-rw-r--r-- | src/NXP/Exception/IncorrectFunctionParameterException.php | 16 | ||||
-rw-r--r-- | src/NXP/MathExecutor.php | 13 | ||||
-rw-r--r-- | tests/MathTest.php | 41 |
7 files changed, 116 insertions, 28 deletions
diff --git a/src/NXP/Classes/Calculator.php b/src/NXP/Classes/Calculator.php index 10adc24..6b63503 100644 --- a/src/NXP/Classes/Calculator.php +++ b/src/NXP/Classes/Calculator.php @@ -74,7 +74,7 @@ class Calculator if (! \array_key_exists($token->value, $this->functions)) { throw new UnknownFunctionException($token->value); } - $stack[] = $this->functions[$token->value]->execute($stack); + $stack[] = $this->functions[$token->value]->execute($stack, $token->paramCount); } elseif (Token::Operator === $token->type) { if (! \array_key_exists($token->value, $this->operators)) { throw new UnknownOperatorException($token->value); diff --git a/src/NXP/Classes/CustomFunction.php b/src/NXP/Classes/CustomFunction.php index 843cf82..1ebdd2c 100644 --- a/src/NXP/Classes/CustomFunction.php +++ b/src/NXP/Classes/CustomFunction.php @@ -2,6 +2,7 @@ namespace NXP\Classes; +use NXP\Exception\IncorrectFunctionParameterException; use NXP\Exception\IncorrectNumberOfFunctionParametersException; use ReflectionException; use ReflectionFunction; @@ -15,41 +16,48 @@ class CustomFunction */ public $function; - public int $places = 0; + private ReflectionFunction $reflectionFunction; /** * CustomFunction constructor. * * @throws ReflectionException - * @throws IncorrectNumberOfFunctionParametersException */ - public function __construct(string $name, callable $function, ?int $places = null) + public function __construct(string $name, callable $function) { $this->name = $name; $this->function = $function; + $this->reflectionFunction = new ReflectionFunction($function); - if (null === $places) { - $reflection = new ReflectionFunction($function); - $this->places = $reflection->getNumberOfParameters(); - } else { - $this->places = $places; - } } /** * @param array<Token> $stack * - * @throws IncorrectNumberOfFunctionParametersException + * @throws IncorrectNumberOfFunctionParametersException|IncorrectFunctionParameterException */ - public function execute(array &$stack) : Token + public function execute(array &$stack, int $paramCountInStack) : Token { - if (\count($stack) < $this->places) { + if ($paramCountInStack < $this->reflectionFunction->getNumberOfRequiredParameters()) { throw new IncorrectNumberOfFunctionParametersException($this->name); } $args = []; - for ($i = 0; $i < $this->places; $i++) { - \array_unshift($args, \array_pop($stack)->value); + if ($paramCountInStack > 0) { + $reflectionParameters = $this->reflectionFunction->getParameters(); + + for ($i = 0; $i < $paramCountInStack; $i++) { + $value = \array_pop($stack)->value; + $valueType = \gettype($value); + $reflectionParameter = $reflectionParameters[\min(\count($reflectionParameters) - 1, $i)]; + //TODO to support type check for union types (php >= 8.0) and intersection types (php >= 8.1), we should increase min php level in composer.json + // For now, only support basic types. @see testFunctionParameterTypes + if ($reflectionParameter->hasType() && $reflectionParameter->getType()->getName() !== $valueType){ + throw new IncorrectFunctionParameterException(); + } + + \array_unshift($args, $value); + } } $result = \call_user_func_array($this->function, $args); diff --git a/src/NXP/Classes/Token.php b/src/NXP/Classes/Token.php index e75b17e..7532e77 100644 --- a/src/NXP/Classes/Token.php +++ b/src/NXP/Classes/Token.php @@ -28,6 +28,8 @@ class Token public ?string $name; + public ?int $paramCount = null;//to store function parameter count in stack + /** * Token constructor. * diff --git a/src/NXP/Classes/Tokenizer.php b/src/NXP/Classes/Tokenizer.php index 32404a2..23e1cc9 100644 --- a/src/NXP/Classes/Tokenizer.php +++ b/src/NXP/Classes/Tokenizer.php @@ -20,7 +20,7 @@ use SplStack; */ class Tokenizer { - /** @var array<Token> */ + /** @var array<Token> */ public array $tokens = []; private string $input = ''; @@ -31,7 +31,7 @@ class Tokenizer private bool $allowNegative = true; - /** @var array<Operator> */ + /** @var array<Operator> */ private array $operators = []; private bool $inSingleQuotedString = false; @@ -99,8 +99,8 @@ class Tokenizer break; } - // no break - // Intentionally fall through + // no break + // Intentionally fall through case $this->isAlpha($ch): if (\strlen($this->numberBuffer)) { $this->emptyNumberBufferAsLiteral(); @@ -196,8 +196,8 @@ class Tokenizer } /** - * @throws IncorrectBracketsException * @throws UnknownOperatorException + * @throws IncorrectBracketsException * @return Token[] Array of tokens in revers polish notation */ public function buildReversePolishNotation() : array @@ -205,6 +205,10 @@ class Tokenizer $tokens = []; /** @var SplStack<Token> $stack */ $stack = new SplStack(); + /** + * @var SplStack<int> $paramCounter + */ + $paramCounter = new SplStack(); foreach ($this->tokens as $token) { switch ($token->type) { @@ -213,9 +217,21 @@ class Tokenizer case Token::String: $tokens[] = $token; + if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) { + $paramCounter->push($paramCounter->pop() + 1); + } + break; case Token::Function: + if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) { + $paramCounter->push($paramCounter->pop() + 1); + } + $stack->push($token); + $paramCounter->push(0); + + break; + case Token::LeftParenthesis: $stack->push($token); @@ -228,6 +244,7 @@ class Tokenizer } $tokens[] = $stack->pop(); } + $paramCounter->push($paramCounter->pop() + 1); break; @@ -270,7 +287,12 @@ class Tokenizer } if ($stack->count() > 0 && Token::Function == $stack->top()->type) { - $tokens[] = $stack->pop(); + /** + * @var Token $f + */ + $f = $stack->pop(); + $f->paramCount = $paramCounter->pop(); + $tokens[] = $f; } break; diff --git a/src/NXP/Exception/IncorrectFunctionParameterException.php b/src/NXP/Exception/IncorrectFunctionParameterException.php new file mode 100644 index 0000000..b378718 --- /dev/null +++ b/src/NXP/Exception/IncorrectFunctionParameterException.php @@ -0,0 +1,16 @@ +<?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; + +class IncorrectFunctionParameterException extends MathExecutorException +{ +} diff --git a/src/NXP/MathExecutor.php b/src/NXP/MathExecutor.php index d2bcfe6..b3e539b 100644 --- a/src/NXP/MathExecutor.php +++ b/src/NXP/MathExecutor.php @@ -116,15 +116,14 @@ class MathExecutor * * @param string $name Name of function * @param callable|null $function Function - * @param int|null $places Count of arguments * - * @throws Exception\IncorrectNumberOfFunctionParametersException * @throws ReflectionException + * @throws Exception\IncorrectNumberOfFunctionParametersException * @return MathExecutor */ - public function addFunction(string $name, ?callable $function = null, ?int $places = null) : self + public function addFunction(string $name, ?callable $function = null) : self { - $this->functions[$name] = new CustomFunction($name, $function, $places); + $this->functions[$name] = new CustomFunction($name, $function); return $this; } @@ -432,13 +431,13 @@ class MathExecutor 'log' => static fn($arg) => \log($arg), 'log10' => static fn($arg) => \log10($arg), 'log1p' => static fn($arg) => \log1p($arg), - 'max' => static fn($arg1, $arg2) => \max($arg1, $arg2), - 'min' => static fn($arg1, $arg2) => \min($arg1, $arg2), + 'max' => static fn($arg1, $arg2, ...$args) => \max($arg1, $arg2, ...$args), + 'min' => static fn($arg1, $arg2, ...$args) => \min($arg1, $arg2, ...$args), 'octdec' => static fn($arg) => \octdec($arg), 'pi' => static fn() => M_PI, 'pow' => static fn($arg1, $arg2) => $arg1 ** $arg2, 'rad2deg' => static fn($arg) => \rad2deg($arg), - 'round' => static fn($arg) => \round($arg), + 'round' => static fn($num, int $precision = 0) => \round($num, $precision), 'sin' => static fn($arg) => \sin($arg), 'sinh' => static fn($arg) => \sinh($arg), 'sec' => static fn($arg) => 1 / \cos($arg), diff --git a/tests/MathTest.php b/tests/MathTest.php index f82452b..e616721 100644 --- a/tests/MathTest.php +++ b/tests/MathTest.php @@ -14,6 +14,7 @@ namespace NXP\Tests; use Exception; use NXP\Exception\DivisionByZeroException; use NXP\Exception\IncorrectExpressionException; +use NXP\Exception\IncorrectFunctionParameterException; use NXP\Exception\IncorrectNumberOfFunctionParametersException; use NXP\Exception\MathExecutorException; use NXP\Exception\UnknownFunctionException; @@ -243,6 +244,7 @@ class MathTest extends TestCase ['-(4*-2)-5'], ['-(-4*2) - 5'], ['-4*-5'], + ['max(1,2,4.9,3)'] ]; } @@ -323,6 +325,45 @@ class MathTest extends TestCase $this->assertEquals(\round(100 / 30), $calculator->execute('round(100/30)')); } + public function testFunctionUnlimitedParameters() : void + { + $calculator = new MathExecutor(); + $calculator->addFunction('max', static function($arg1, $arg2, ...$args) { + return \max($arg1, $arg2, ...$args); + }); + $this->assertEquals(\max(4, 6, 8.1, 2, 7), $calculator->execute('max(4,6,8.1,2,7)')); + } + + public function testFunctionOptionalParameters() : void + { + $calculator = new MathExecutor(); + $calculator->addFunction('round', static function($num, $precision = 0) { + return \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)')); + } + + public function testFunctionParameterTypes() : void + { + $calculator = new MathExecutor(); + $this->expectException(IncorrectFunctionParameterException::class); + $calculator->addFunction('myfunc', static function(string $name, int $age) { + return $name . $age; + }); + $calculator->execute('myfunc(22, "John Doe")'); + } + + public function testFunctionIncorrectNumberOfParameters() : void + { + $calculator = new MathExecutor(); + $this->expectException(IncorrectNumberOfFunctionParametersException::class); + $calculator->addFunction('myfunc', static function($arg1, $arg2) { + return $arg1 + $arg2; + }); + $calculator->execute('myfunc(1)'); + } + public function testFunctionIf() : void { $calculator = new MathExecutor(); |