Added ability to escape quotes in strings. (#110)

* Added ability to escape quotes in strings.

* Removed type checking for customfunc arguments. It was a bad idea to check types, because php automatically tries to convert a parameter to required type and throws if it failures. On the other hand, we can check types also in callables if required.

* Update phpdoc

* Fix some typos + improve min, max, avg funcs.

* Update readme + improvements.

* Fix a typo in sample.

* Fix unshown backslash in readme.
This commit is contained in:
Fatih Kızmaz 2022-05-19 05:03:44 +03:00 committed by GitHub
parent f71b77a62e
commit 3e6700d157
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 114 additions and 68 deletions

View file

@ -7,7 +7,7 @@
* Paratheses () and arrays [] are fully supported
* Logical operators (==, !=, <, <, >=, <=, &&, ||)
* Built in support for most PHP math functions
* Support for variable number of function parameters
* Support for variable number of function parameters and optional function parameters
* Conditional If logic
* Support for user defined operators
* Support for user defined functions
@ -87,9 +87,17 @@ Add custom function to executor:
```php
$executor->addFunction('abs', function($arg) {return abs($arg);});
```
Function default parameters (optional parameters) are also supported.
Optional parameters:
```php
$executor->addFunction('round', function($num, int $precision = 0) {return round($num, $precision);});
$executor->calculate('round(17.119)'); // 17
$executor->calculate('round(17.119, 2)'); // 17.12
```
Variable number of parameters:
```php
$executor->addFunction('avarage', function(...$args) {return array_sum($args) / count($args);});
$executor->calculate('avarage(1,3)'); // 2
$executor->calculate('avarage(1, 3, 4, 8)'); // 4
```
## Operators:
@ -211,6 +219,11 @@ Expressions can contain double or single quoted strings that are evaluated the s
```php
echo $executor->execute("1 + '2.5' * '.5' + myFunction('category')");
```
To use reverse solidus character (&#92;) in strings, or to use single quote character (') in a single quoted string, or to use double quote character (") in a double quoted string, you must prepend reverse solidus character (&#92;).
```php
echo $executor->execute("countArticleSentences('My Best Article\'s Title')");
```
## Extending MathExecutor
You can add operators, functions and variables with the public methods in MathExecutor, but if you need to do more serious modifications to base behaviors, the easiest way to extend MathExecutor is to redefine the following methods in your derived class:

View file

@ -2,7 +2,6 @@
namespace NXP\Classes;
use NXP\Exception\IncorrectFunctionParameterException;
use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use ReflectionException;
use ReflectionFunction;
@ -16,7 +15,7 @@ class CustomFunction
*/
public $function;
private ReflectionFunction $reflectionFunction;
private int $requiredParamCount;
/**
* CustomFunction constructor.
@ -27,36 +26,25 @@ class CustomFunction
{
$this->name = $name;
$this->function = $function;
$this->reflectionFunction = new ReflectionFunction($function);
$this->requiredParamCount = (new ReflectionFunction($function))->getNumberOfRequiredParameters();
}
/**
* @param array<Token> $stack
*
* @throws IncorrectNumberOfFunctionParametersException|IncorrectFunctionParameterException
* @throws IncorrectNumberOfFunctionParametersException
*/
public function execute(array &$stack, int $paramCountInStack) : Token
{
if ($paramCountInStack < $this->reflectionFunction->getNumberOfRequiredParameters()) {
if ($paramCountInStack < $this->requiredParamCount) {
throw new IncorrectNumberOfFunctionParametersException($this->name);
}
$args = [];
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);
\array_unshift($args, \array_pop($stack)->value);
}
}

View file

@ -50,28 +50,68 @@ class Tokenizer
public function tokenize() : self
{
foreach (\str_split($this->input, 1) as $ch) {
$isLastCharEscape = false;
foreach (\str_split($this->input) as $ch) {
switch (true) {
case $this->inSingleQuotedString:
if ("'" === $ch) {
if ('\\' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
} else {
$isLastCharEscape = true;
}
continue 2;
} elseif ("'" === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= "'";
$isLastCharEscape = false;
} else {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inSingleQuotedString = false;
$this->stringBuffer = '';
}
continue 2;
}
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
}
$this->stringBuffer .= $ch;
continue 2;
case $this->inDoubleQuotedString:
if ('"' === $ch) {
if ('\\' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
} else {
$isLastCharEscape = true;
}
continue 2;
} elseif ('"' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '"';
$isLastCharEscape = false;
} else {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inDoubleQuotedString = false;
$this->stringBuffer = '';
}
continue 2;
}
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
}
$this->stringBuffer .= $ch;
continue 2;

View file

@ -16,7 +16,6 @@ use NXP\Classes\Operator;
use NXP\Classes\Token;
use NXP\Classes\Tokenizer;
use NXP\Exception\DivisionByZeroException;
use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use NXP\Exception\MathExecutorException;
use NXP\Exception\UnknownVariableException;
use ReflectionException;
@ -386,19 +385,21 @@ class MathExecutor
'arccos' => static fn($arg) => \acos($arg),
'arctan' => static fn($arg) => \atan($arg),
'arctg' => static fn($arg) => \atan($arg),
'array' => static fn(...$args) => $args,
'asin' => static fn($arg) => \asin($arg),
'atan' => static fn($arg) => \atan($arg),
'atan2' => static fn($arg1, $arg2) => \atan2($arg1, $arg2),
'atanh' => static fn($arg) => \atanh($arg),
'atn' => static fn($arg) => \atan($arg),
'avg' => static function($arg1, $args) {
'avg' => static function($arg1, ...$args) {
if (\is_array($arg1)){
if (0 === \count($arg1)){
throw new \InvalidArgumentException('Array must contain at least one element!');
}
return \array_sum($arg1) / \count($arg1);
}
if (0 === \count($args)){
throw new IncorrectNumberOfFunctionParametersException();
}
$args = [$arg1, ...$args];
return \array_sum($args) / \count($args);
@ -444,18 +445,18 @@ class MathExecutor
'log10' => static fn($arg) => \log10($arg),
'log1p' => static fn($arg) => \log1p($arg),
'max' => static function($arg1, ...$args) {
if (! \is_array($arg1) && 0 === \count($args)){
throw new IncorrectNumberOfFunctionParametersException();
if (\is_array($arg1) && 0 === \count($arg1)){
throw new \InvalidArgumentException('Array must contain at least one element!');
}
return \max($arg1, ...$args);
return \max(\is_array($arg1) ? $arg1 : [$arg1, ...$args]);
},
'min' => static function($arg1, ...$args) {
if (! \is_array($arg1) && 0 === \count($args)){
throw new IncorrectNumberOfFunctionParametersException();
if (\is_array($arg1) && 0 === \count($arg1)){
throw new \InvalidArgumentException('Array must contain at least one element!');
}
return \min($arg1, ...$args);
return \min(\is_array($arg1) ? $arg1 : [$arg1, ...$args]);
},
'octdec' => static fn($arg) => \octdec($arg),
'pi' => static fn() => M_PI,
@ -469,8 +470,7 @@ class MathExecutor
'tan' => static fn($arg) => \tan($arg),
'tanh' => static fn($arg) => \tanh($arg),
'tn' => static fn($arg) => \tan($arg),
'tg' => static fn($arg) => \tan($arg),
'array' => static fn(...$args) => [...$args]
'tg' => static fn($arg) => \tan($arg)
];
}
@ -488,7 +488,7 @@ class MathExecutor
}
/**
* Default variable validation, ensures that the value is a scalar.
* 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

View file

@ -14,7 +14,6 @@ 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;
@ -244,7 +243,10 @@ class MathTest extends TestCase
['-(4*-2)-5'],
['-(-4*2) - 5'],
['-4*-5'],
['max(1,2,4.9,3)']
['max(1,2,4.9,3)'],
['min(1,2,4.9,3)'],
['max([1,2,4.9,3])'],
['min([1,2,4.9,3])']
];
}
@ -305,6 +307,29 @@ class MathTest extends TestCase
$this->assertEquals(100, $calculator->execute('10 ^ 2'));
}
public function testStringEscape() : void
{
$calculator = new MathExecutor();
$this->assertEquals("test\string", $calculator->execute('"test\string"'));
$this->assertEquals("\\test\string\\", $calculator->execute('"\test\string\\\\"'));
$this->assertEquals('\test\string\\', $calculator->execute('"\test\string\\\\"'));
$this->assertEquals('test\\\\string', $calculator->execute('"test\\\\\\\\string"'));
$this->assertEquals('test"string', $calculator->execute('"test\"string"'));
$this->assertEquals('test""string', $calculator->execute('"test\"\"string"'));
$this->assertEquals('"teststring', $calculator->execute('"\"teststring"'));
$this->assertEquals('teststring"', $calculator->execute('"teststring\""'));
$this->assertEquals("test'string", $calculator->execute("'test\'string'"));
$this->assertEquals("test''string", $calculator->execute("'test\'\'string'"));
$this->assertEquals("'teststring", $calculator->execute("'\'teststring'"));
$this->assertEquals("teststring'", $calculator->execute("'teststring\''"));
$calculator->addFunction('concat', static function($arg1, $arg2) {
return $arg1 . $arg2;
});
$this->assertEquals('test"ing', $calculator->execute('concat("test\"","ing")'));
$this->assertEquals("test'ing", $calculator->execute("concat('test\'','ing')"));
}
public function testArrays() : void
{
$calculator = new MathExecutor();
@ -347,26 +372,16 @@ class MathTest extends TestCase
$calculator->addFunction('give_me_an_array', static function() {
return [5, 3, 7, 9, 8];
});
$calculator->addFunction('my_avarage', static function($arg1, ...$args) {
if (\is_array($arg1)){
return \array_sum($arg1) / \count($arg1);
}
if (0 === \count($args)){
throw new IncorrectNumberOfFunctionParametersException();
}
$args = [$arg1, ...$args];
return \array_sum($args) / \count($args);
});
$this->assertEquals(10, $calculator->execute('my_avarage(12,8,15,5)'));
$this->assertEquals(6.4, $calculator->execute('my_avarage(give_me_an_array())'));
$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())'));
$this->assertEquals(1, $calculator->execute('min(1,2,3)'));
$this->assertEquals(9, $calculator->execute('max(give_me_an_array())'));
$this->assertEquals(3, $calculator->execute('max(1,2,3)'));
$calculator->setVar('monthly_salaries', [100, 200, 300]);
$this->assertEquals([100, 200, 300], $calculator->execute('$monthly_salaries'));
$this->assertEquals(200, $calculator->execute('avg($monthly_salaries)'));
$this->assertEquals(\min([100, 200, 300]), $calculator->execute('min($monthly_salaries)'));
$this->assertEquals(\max([100, 200, 300]), $calculator->execute('max($monthly_salaries)'));
}
@ -380,16 +395,6 @@ class MathTest extends TestCase
$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();