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:
parent
da506a7ce0
commit
e21d59c9de
7 changed files with 116 additions and 28 deletions
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
||||||
|
|
16
src/NXP/Exception/IncorrectFunctionParameterException.php
Normal file
16
src/NXP/Exception/IncorrectFunctionParameterException.php
Normal 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
|
||||||
|
{
|
||||||
|
}
|
|
@ -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),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue