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
This commit is contained in:
Fatih Kızmaz 2022-05-13 15:55:52 +03:00 committed by GitHub
parent da506a7ce0
commit e21d59c9de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 116 additions and 28 deletions

View file

@ -74,7 +74,7 @@ class Calculator
if (! \array_key_exists($token->value, $this->functions)) { if (! \array_key_exists($token->value, $this->functions)) {
throw new UnknownFunctionException($token->value); 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) { } elseif (Token::Operator === $token->type) {
if (! \array_key_exists($token->value, $this->operators)) { if (! \array_key_exists($token->value, $this->operators)) {
throw new UnknownOperatorException($token->value); throw new UnknownOperatorException($token->value);

View file

@ -2,6 +2,7 @@
namespace NXP\Classes; namespace NXP\Classes;
use NXP\Exception\IncorrectFunctionParameterException;
use NXP\Exception\IncorrectNumberOfFunctionParametersException; use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use ReflectionException; use ReflectionException;
use ReflectionFunction; use ReflectionFunction;
@ -15,41 +16,48 @@ class CustomFunction
*/ */
public $function; public $function;
public int $places = 0; private ReflectionFunction $reflectionFunction;
/** /**
* CustomFunction constructor. * CustomFunction constructor.
* *
* @throws ReflectionException * @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->name = $name;
$this->function = $function; $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 * @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); throw new IncorrectNumberOfFunctionParametersException($this->name);
} }
$args = []; $args = [];
for ($i = 0; $i < $this->places; $i++) { if ($paramCountInStack > 0) {
\array_unshift($args, \array_pop($stack)->value); $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); $result = \call_user_func_array($this->function, $args);

View file

@ -28,6 +28,8 @@ class Token
public ?string $name; public ?string $name;
public ?int $paramCount = null;//to store function parameter count in stack
/** /**
* Token constructor. * Token constructor.
* *

View file

@ -20,7 +20,7 @@ use SplStack;
*/ */
class Tokenizer class Tokenizer
{ {
/** @var array<Token> */ /** @var array<Token> */
public array $tokens = []; public array $tokens = [];
private string $input = ''; private string $input = '';
@ -31,7 +31,7 @@ class Tokenizer
private bool $allowNegative = true; private bool $allowNegative = true;
/** @var array<Operator> */ /** @var array<Operator> */
private array $operators = []; private array $operators = [];
private bool $inSingleQuotedString = false; private bool $inSingleQuotedString = false;
@ -99,8 +99,8 @@ class Tokenizer
break; break;
} }
// no break // no break
// Intentionally fall through // Intentionally fall through
case $this->isAlpha($ch): case $this->isAlpha($ch):
if (\strlen($this->numberBuffer)) { if (\strlen($this->numberBuffer)) {
$this->emptyNumberBufferAsLiteral(); $this->emptyNumberBufferAsLiteral();
@ -196,8 +196,8 @@ class Tokenizer
} }
/** /**
* @throws IncorrectBracketsException
* @throws UnknownOperatorException * @throws UnknownOperatorException
* @throws IncorrectBracketsException
* @return Token[] Array of tokens in revers polish notation * @return Token[] Array of tokens in revers polish notation
*/ */
public function buildReversePolishNotation() : array public function buildReversePolishNotation() : array
@ -205,6 +205,10 @@ class Tokenizer
$tokens = []; $tokens = [];
/** @var SplStack<Token> $stack */ /** @var SplStack<Token> $stack */
$stack = new SplStack(); $stack = new SplStack();
/**
* @var SplStack<int> $paramCounter
*/
$paramCounter = new SplStack();
foreach ($this->tokens as $token) { foreach ($this->tokens as $token) {
switch ($token->type) { switch ($token->type) {
@ -213,9 +217,21 @@ class Tokenizer
case Token::String: case Token::String:
$tokens[] = $token; $tokens[] = $token;
if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) {
$paramCounter->push($paramCounter->pop() + 1);
}
break; break;
case Token::Function: 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: case Token::LeftParenthesis:
$stack->push($token); $stack->push($token);
@ -228,6 +244,7 @@ class Tokenizer
} }
$tokens[] = $stack->pop(); $tokens[] = $stack->pop();
} }
$paramCounter->push($paramCounter->pop() + 1);
break; break;
@ -270,7 +287,12 @@ class Tokenizer
} }
if ($stack->count() > 0 && Token::Function == $stack->top()->type) { 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; break;

View file

@ -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
{
}

View file

@ -116,15 +116,14 @@ class MathExecutor
* *
* @param string $name Name of function * @param string $name Name of function
* @param callable|null $function Function * @param callable|null $function Function
* @param int|null $places Count of arguments
* *
* @throws Exception\IncorrectNumberOfFunctionParametersException
* @throws ReflectionException * @throws ReflectionException
* @throws Exception\IncorrectNumberOfFunctionParametersException
* @return MathExecutor * @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; return $this;
} }
@ -432,13 +431,13 @@ class MathExecutor
'log' => static fn($arg) => \log($arg), 'log' => static fn($arg) => \log($arg),
'log10' => static fn($arg) => \log10($arg), 'log10' => static fn($arg) => \log10($arg),
'log1p' => static fn($arg) => \log1p($arg), 'log1p' => static fn($arg) => \log1p($arg),
'max' => static fn($arg1, $arg2) => \max($arg1, $arg2), 'max' => static fn($arg1, $arg2, ...$args) => \max($arg1, $arg2, ...$args),
'min' => static fn($arg1, $arg2) => \min($arg1, $arg2), 'min' => static fn($arg1, $arg2, ...$args) => \min($arg1, $arg2, ...$args),
'octdec' => static fn($arg) => \octdec($arg), 'octdec' => static fn($arg) => \octdec($arg),
'pi' => static fn() => M_PI, 'pi' => static fn() => M_PI,
'pow' => static fn($arg1, $arg2) => $arg1 ** $arg2, 'pow' => static fn($arg1, $arg2) => $arg1 ** $arg2,
'rad2deg' => static fn($arg) => \rad2deg($arg), '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), 'sin' => static fn($arg) => \sin($arg),
'sinh' => static fn($arg) => \sinh($arg), 'sinh' => static fn($arg) => \sinh($arg),
'sec' => static fn($arg) => 1 / \cos($arg), 'sec' => static fn($arg) => 1 / \cos($arg),

View file

@ -14,6 +14,7 @@ namespace NXP\Tests;
use Exception; use Exception;
use NXP\Exception\DivisionByZeroException; use NXP\Exception\DivisionByZeroException;
use NXP\Exception\IncorrectExpressionException; use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\IncorrectFunctionParameterException;
use NXP\Exception\IncorrectNumberOfFunctionParametersException; use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use NXP\Exception\MathExecutorException; use NXP\Exception\MathExecutorException;
use NXP\Exception\UnknownFunctionException; use NXP\Exception\UnknownFunctionException;
@ -243,6 +244,7 @@ class MathTest extends TestCase
['-(4*-2)-5'], ['-(4*-2)-5'],
['-(-4*2) - 5'], ['-(-4*2) - 5'],
['-4*-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)')); $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 public function testFunctionIf() : void
{ {
$calculator = new MathExecutor(); $calculator = new MathExecutor();