From e21d59c9def9f25bc6a7c3fbd41e2e126d68b1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20K=C4=B1zmaz?= Date: Fri, 13 May 2022 15:55:52 +0300 Subject: [PATCH] Support unlimited args for min, max default funcs. (#106) * Support unlimited args for min, max default funcs. Default functions max and min were requiring 2 arguments strictly. Now they supoort unlimited args, same as php's min, max funcs. * Improved functions: support unlimited parameters (see min, max funcs), optional parameters (see round func), parameters with types (see round func, throws IncorrectFunctionParameterException on unmatched type, union types and intersection types not supported because of min php level! there is a todo for this, to support them later @see CustomFunction@execute) Also added unittests for improvements. * Run php-cs-fixer fix --- src/NXP/Classes/Calculator.php | 2 +- src/NXP/Classes/CustomFunction.php | 36 +++++++++------- src/NXP/Classes/Token.php | 2 + src/NXP/Classes/Tokenizer.php | 34 ++++++++++++--- .../IncorrectFunctionParameterException.php | 16 ++++++++ src/NXP/MathExecutor.php | 13 +++--- tests/MathTest.php | 41 +++++++++++++++++++ 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 src/NXP/Exception/IncorrectFunctionParameterException.php 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 $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 */ + /** @var array */ public array $tokens = []; private string $input = ''; @@ -31,7 +31,7 @@ class Tokenizer private bool $allowNegative = true; - /** @var array */ + /** @var array */ 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 $stack */ $stack = new SplStack(); + /** + * @var SplStack $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 @@ +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();