From 66c9ead812d56b832470c06b506597f60b4989c5 Mon Sep 17 00:00:00 2001 From: Stefan Ackermann Date: Fri, 17 Apr 2020 16:42:59 +0200 Subject: [PATCH] make null handling configurable, static interface, typed exception --- .../Exceptions/InvalidSyntaxException.php | 11 + lib/MathParser/Expression.php | 34 +-- lib/MathParser/Expressions/Addition.php | 26 +- lib/MathParser/Expressions/Division.php | 34 ++- lib/MathParser/Expressions/Modulo.php | 34 ++- lib/MathParser/Expressions/Multiplication.php | 26 +- lib/MathParser/Expressions/Number.php | 9 +- lib/MathParser/Expressions/Operator.php | 12 +- lib/MathParser/Expressions/Parenthesis.php | 17 +- lib/MathParser/Expressions/Power.php | 31 ++- lib/MathParser/Expressions/Subtraction.php | 26 +- lib/MathParser/Expressions/Unary.php | 15 +- lib/MathParser/Math.php | 116 ++++---- lib/MathParser/Options/NullHandling.php | 43 +++ lib/MathParser/Variable.php | 11 +- test/MathParserTest.php | 249 ++++++++++++++++-- 16 files changed, 493 insertions(+), 201 deletions(-) create mode 100644 lib/MathParser/Exceptions/InvalidSyntaxException.php create mode 100644 lib/MathParser/Options/NullHandling.php diff --git a/lib/MathParser/Exceptions/InvalidSyntaxException.php b/lib/MathParser/Exceptions/InvalidSyntaxException.php new file mode 100644 index 0000000..039d17d --- /dev/null +++ b/lib/MathParser/Exceptions/InvalidSyntaxException.php @@ -0,0 +1,11 @@ +value = $value; } - public static function factory($value) + public static function factory($value): Expression { if (is_object($value) && $value instanceof self) { return $value; } elseif (is_numeric($value)) { // +0 => to number conversion (int or float) return new Number($value + 0); - } elseif ($value == 'u') { + } elseif ($value === 'u') { return new Unary($value); - } elseif ($value == '+') { + } elseif ($value === '+') { return new Addition($value); - } elseif ($value == '-') { + } elseif ($value === '-') { return new Subtraction($value); - } elseif ($value == '*') { + } elseif ($value === '*') { return new Multiplication($value); - } elseif ($value == '/') { + } elseif ($value === '/') { return new Division($value); - } elseif (in_array($value, array('(', ')'))) { + } elseif (in_array($value, ['(', ')'], true)) { return new Parenthesis($value); - } elseif ($value == '^') { + } elseif ($value === '^') { return new Power($value); - } elseif ($value == '%') { + } elseif ($value === '%') { return new Modulo($value); - } elseif (strlen($value) >= 2 && $value[0] == '$') { + } elseif (strlen($value) >= 2 && $value[0] === '$') { return new Variable(substr($value, 1)); } throw new \RuntimeException('Undefined Value ' . $value); } - abstract public function operate(array &$stack); + abstract public function operate(array &$stack, array $options); - public function isOperator() + public function isOperator(): bool { return false; } - public function isUnary() + public function isUnary(): bool { return false; } - public function isParenthesis() + public function isParenthesis(): bool { return false; } - public function isNoOp() + public function isNoOp(): bool { return false; } - public function isVariable() + public function isVariable(): bool { return false; } diff --git a/lib/MathParser/Expressions/Addition.php b/lib/MathParser/Expressions/Addition.php index 2378b5d..e472102 100644 --- a/lib/MathParser/Expressions/Addition.php +++ b/lib/MathParser/Expressions/Addition.php @@ -1,20 +1,26 @@ operate($stack); - $right = array_pop($stack)->operate($stack); - - if ($left === null || $right === null) { - return null; - } - - return $left + $right; + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + return $left + $right; + } + ); } } diff --git a/lib/MathParser/Expressions/Division.php b/lib/MathParser/Expressions/Division.php index 08154f0..3631dc9 100644 --- a/lib/MathParser/Expressions/Division.php +++ b/lib/MathParser/Expressions/Division.php @@ -1,24 +1,30 @@ operate($stack); - $right = array_pop($stack)->operate($stack); - - if ($left === null || $right === null) { - return null; - } - - if ($left === 0) { - return null; - } - - return $right / $left; + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + if ($right === 0) { + return null; + } + + return $left / $right; + } + ); } } diff --git a/lib/MathParser/Expressions/Modulo.php b/lib/MathParser/Expressions/Modulo.php index 1de4429..fa683f2 100644 --- a/lib/MathParser/Expressions/Modulo.php +++ b/lib/MathParser/Expressions/Modulo.php @@ -1,24 +1,30 @@ operate($stack); - $right = array_pop($stack)->operate($stack); - - if ($left === null || $right === null) { - return null; - } - - if ($left === 0) { - return null; - } else { - return $right % $left; - } + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + if ($right === 0) { + return null; + } else { + return $left % $right; + } + } + ); } } diff --git a/lib/MathParser/Expressions/Multiplication.php b/lib/MathParser/Expressions/Multiplication.php index e8d0000..2d96f9e 100644 --- a/lib/MathParser/Expressions/Multiplication.php +++ b/lib/MathParser/Expressions/Multiplication.php @@ -1,18 +1,26 @@ operate($stack); - $right = array_pop($stack)->operate($stack); - if ($left === null || $right === null) { - return null; - } - return $left * $right; + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + return $left * $right; + } + ); } } diff --git a/lib/MathParser/Expressions/Number.php b/lib/MathParser/Expressions/Number.php index 7b49ecc..1aa0786 100644 --- a/lib/MathParser/Expressions/Number.php +++ b/lib/MathParser/Expressions/Number.php @@ -1,17 +1,14 @@ value; } diff --git a/lib/MathParser/Expressions/Operator.php b/lib/MathParser/Expressions/Operator.php index d415594..5fefcd1 100644 --- a/lib/MathParser/Expressions/Operator.php +++ b/lib/MathParser/Expressions/Operator.php @@ -1,25 +1,23 @@ precedence; - } + abstract public function getPrecedence(): int; - public function isLeftAssoc() + public function isLeftAssoc(): bool { return $this->leftAssoc; } - public function isOperator() + public function isOperator(): bool { return true; } diff --git a/lib/MathParser/Expressions/Parenthesis.php b/lib/MathParser/Expressions/Parenthesis.php index f866028..bd032d1 100644 --- a/lib/MathParser/Expressions/Parenthesis.php +++ b/lib/MathParser/Expressions/Parenthesis.php @@ -1,34 +1,35 @@ precedence; + return 6; } - public function isNoOp() + public function isNoOp(): bool { return true; } - public function isParenthesis() + public function isParenthesis(): bool { return true; } - public function isOpen() + public function isOpen(): bool { - return $this->value == '('; + return $this->value === '('; } } diff --git a/lib/MathParser/Expressions/Power.php b/lib/MathParser/Expressions/Power.php index 8fbb508..5dae65e 100644 --- a/lib/MathParser/Expressions/Power.php +++ b/lib/MathParser/Expressions/Power.php @@ -1,19 +1,32 @@ operate($stack); - $left = array_pop($stack)->operate($stack); - - if ($left === null || $right === null) { - return null; - } - return pow($left, $right); + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + return $left ** $right; + } + ); } } diff --git a/lib/MathParser/Expressions/Subtraction.php b/lib/MathParser/Expressions/Subtraction.php index 0ae841e..266f5f6 100644 --- a/lib/MathParser/Expressions/Subtraction.php +++ b/lib/MathParser/Expressions/Subtraction.php @@ -1,20 +1,26 @@ operate($stack); - $right = array_pop($stack)->operate($stack); + return 4; + } - if ($left === null || $right === null) { - return null; - } - - return $right - $left; + public function operate(array &$stack, array $options) + { + return NullHandling::withNullHandling( + $stack, + $options, + static function ($left, $right) { + return $left - $right; + } + ); } } diff --git a/lib/MathParser/Expressions/Unary.php b/lib/MathParser/Expressions/Unary.php index 56ea1bc..d3d1a82 100644 --- a/lib/MathParser/Expressions/Unary.php +++ b/lib/MathParser/Expressions/Unary.php @@ -1,20 +1,25 @@ operate($stack); + $next = array_pop($stack)->operate($stack, $options); if ($next === null) { return null; diff --git a/lib/MathParser/Math.php b/lib/MathParser/Math.php index e3b22e4..f0c2276 100644 --- a/lib/MathParser/Math.php +++ b/lib/MathParser/Math.php @@ -1,34 +1,35 @@ parse($string); - - return $this->run($stack); + $stack = self::parse($string); + self::substituteVariables($stack, $variables); + return self::run($stack, $options); } /** - * @param $string + * @param string $string * @return Expression[] $output */ - public function parse($string) + public static function parse(string $string): array { - $tokens = $this->tokenize($string); + $tokens = self::tokenize($string); $output = []; $operators = []; @@ -38,20 +39,18 @@ public function parse($string) $expression = Expression::factory($token); if ($expression->isOperator()) { if ($expectOperator) { - $this->parseOperator($expression, $output, $operators); + self::parseOperator($expression, $output, $operators); $expectOperator = false; + } elseif (!$expectOperator && $token === '-') { + self::parseOperator(new Unary('u'), $output, $operators); } else { - if (!$expectOperator && $token == '-') { - $this->parseOperator(new Unary('u'), $output, $operators); - } else { - throw new \RuntimeException('expected number or variable but found: ' . get_class($expression)); - } + throw new InvalidSyntaxException('expected number or variable but found: ' . get_class($expression)); } } elseif ($expression->isParenthesis()) { - $this->parseParenthesis($expression, $output, $operators); + self::parseParenthesis($expression, $output, $operators); if ($expression->isOpen()) { if ($expectOperator) { - throw new \RuntimeException('expected operator but found: ' . get_class($expression)); + throw new InvalidSyntaxException('expected operator but found: ' . get_class($expression)); } $expectOperator = false; } else { @@ -59,7 +58,7 @@ public function parse($string) } } else { if ($expectOperator) { - throw new \RuntimeException('expected operator but found: ' . get_class($expression)); + throw new InvalidSyntaxException('expected operator but found: ' . get_class($expression)); } $output[] = $expression; $expectOperator = true; @@ -67,7 +66,7 @@ public function parse($string) } while (($op = array_pop($operators))) { if ($op->isParenthesis()) { - throw new \RuntimeException('Mismatched Parenthesis'); + throw new InvalidSyntaxException('Mismatched Parenthesis'); } $output[] = $op; } @@ -75,28 +74,33 @@ public function parse($string) return $output; } - public function registerVariable($name, $value) - { - $this->assertVariableIsNumberOrNull($value); - - $this->variables[$name] = $value; - } - - public function run(array $stack) + /** + * @param Expression[] $stack + * @param array $options + * @return string|null + */ + public static function run(array $stack, array $options = []) { - $this->substituteVariables($stack, $this->variables); + $defaultOptions = [ + 'null_handling' => 'strict', + 'fallback' => 0 + ]; + $options = $options + $defaultOptions; + + assert(in_array($options['null_handling'], ['strict', 'skip', 'loose', 'fallback'])); + assert(is_int($options['fallback']) || is_float($options['fallback'])); while (($operator = array_pop($stack)) && $operator->isOperator()) { - $value = $operator->operate($stack); - if (!is_null($value)) { + $value = $operator->operate($stack, $options); + if ($value !== null) { $stack[] = Expression::factory($value); } } - return $operator ? $operator->render() : $this->render($stack); + return $operator ? $operator->render() : self::render($stack); } - public function getDistinctVariables(array $stack) + public static function getDistinctVariables(array $stack) { $variables = []; @@ -112,7 +116,7 @@ public function getDistinctVariables(array $stack) return $variables; } - private function render(array &$stack) + private static function render(array &$stack) { $output = ''; while (($el = array_pop($stack))) { @@ -125,7 +129,7 @@ private function render(array &$stack) return null; } - private function parseParenthesis(Expression $expression, array &$output, array &$operators) + private static function parseParenthesis(Expression $expression, array &$output, array &$operators) { if ($expression->isOpen()) { $operators[] = $expression; @@ -145,7 +149,7 @@ private function parseParenthesis(Expression $expression, array &$output, array } } - private function parseOperator(Expression $expression, array &$output, array &$operators) + private static function parseOperator(Expression $expression, array &$output, array &$operators) { $end = end($operators); if (!$end) { @@ -166,44 +170,26 @@ private function parseOperator(Expression $expression, array &$output, array &$o } } - private function tokenize($string) + private static function tokenize(string $string) { $match = preg_match('#^(\d+(\.\d+)?|\$\d+|\$\w+|\+|-|\(|\)|\*|/|%|\^|\s+)+$#', $string); // check to see obvious syntax mistakes (e.g. unallowed characters...) if (!$match) { - throw new \RuntimeException('invalid syntax!'); + throw new InvalidSyntaxException('invalid syntax!'); } - $parts = preg_split('((\d+(?:\.\d+)?|\$\d+|\$\w+|\+|-|\(|\)|\*|/|%|\^|\s+))', $string, null, PREG_SPLIT_NO_EMPTY | + $parts = preg_split('((\d+(?:\.\d+)?|\$\d+|\$\w+|\+|-|\(|\)|\*|/|%|\^|\s+))', $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); - $parts = array_filter(array_map('trim', $parts), function ($val) { + $parts = array_filter(array_map('trim', $parts), static function ($val) { return $val !== ''; }); return $parts; } - /** - * removes any variables - */ - public function clearVariables() - { - $this->variables = []; - } - - /** - * expects a one dimensional array of key-value pairs - * - * @param array $variables - */ - public function setVariables(array $variables) - { - $this->assertVariablesAreNumbersOrNull($variables); - $this->variables = $variables; - } - - private function substituteVariables(array &$stack, array $variables) + public static function substituteVariables(array &$stack, array $variables): void { + self::assertVariablesAreNumbersOrNull($variables); foreach ($stack as &$expression) { if ($expression instanceof Variable) { $expression = new Number($expression->render($variables)); @@ -211,14 +197,14 @@ private function substituteVariables(array &$stack, array $variables) } } - private function assertVariablesAreNumbersOrNull(array $variables) + private static function assertVariablesAreNumbersOrNull(array $variables) { foreach ($variables as $variable) { - $this->assertVariableIsNumberOrNull($variable); + self::assertVariableIsNumberOrNull($variable); } } - private function assertVariableIsNumberOrNull($variable) + private static function assertVariableIsNumberOrNull($variable) { if (!is_int($variable) && !is_float($variable) && !($variable === null)) { throw new \InvalidArgumentException('provided variable is not a number or null. found: ' . diff --git a/lib/MathParser/Options/NullHandling.php b/lib/MathParser/Options/NullHandling.php new file mode 100644 index 0000000..66924b8 --- /dev/null +++ b/lib/MathParser/Options/NullHandling.php @@ -0,0 +1,43 @@ +operate($stack, $options); + $left = array_pop($stack)->operate($stack, $options); + + if ($options['null_handling'] === 'strict') { + if ($left === null || $right === null) { + return null; + } + } elseif ($options['null_handling'] === 'fallback') { + if ($left === null) { + $left = $options['fallback']; + } + if ($right === null) { + $right = $options['fallback']; + } + } elseif ($options['null_handling'] === 'loose') { + if ($left === null) { + $left = 0; + } + if ($right === null) { + $right = 0; + } + } elseif ($options['null_handling'] === 'skip') { + if ($right === null) { + return $left; + } + if ($left === null) { + return $right; + } + } + + return $callback($left, $right); + } +} \ No newline at end of file diff --git a/lib/MathParser/Variable.php b/lib/MathParser/Variable.php index 4e05c2b..75ff49d 100644 --- a/lib/MathParser/Variable.php +++ b/lib/MathParser/Variable.php @@ -1,11 +1,14 @@ value])) { return $variables[$this->value]; @@ -14,17 +17,17 @@ public function render($variables = []) } } - public function getName() + public function getName(): string { return $this->value; } - public function isVariable() + public function isVariable(): bool { return true; } - public function operate(array &$stack) + public function operate(array &$stack, array $options) { throw new RuntimeException('variable not instantiated'); } diff --git a/test/MathParserTest.php b/test/MathParserTest.php index 698ac92..2da1c23 100644 --- a/test/MathParserTest.php +++ b/test/MathParserTest.php @@ -1,6 +1,10 @@ null, 'b2' => 41, 0 => 13]], null], + + [['$0 * $0', [null]], null], + [['$0 * $1', [null, 41]], null], + [['$0 * $1', [41, null]], null], + [['$a1 * $b2 * $0', ['a1' => null, 'b2' => 41, 0 => 13]], null], + + [['$0 / $0', [null]], null], + [['$0 / $1', [null, 41]], null], + [['$0 / $1', [41, null]], null], + [['$a1 / $b2 / $0', ['a1' => null, 'b2' => 41, 0 => 13]], null], + + [['$0 % $0', [null]], null], + [['$0 % $1', [null, 41]], null], + [['$0 % $1', [41, null]], null], + [['$a1 % $b2 % $0', ['a1' => null, 'b2' => 41, 0 => 13]], null], + + [['$0 - $0', [null]], null], + [['$0 - $1', [null, 41]], null], + [['$0 - $1', [41, null]], null], + [['$a1 - $b2 - $0', ['a1' => null, 'b2' => 41, 0 => 13]], null], + + [['$0 ^ $0', [null]], null], + [['$0 ^ $1', [null, 41]], null], + [['$0 ^ $1', [41, null]], null], + [['$a1 ^ $b2 ^ $0', ['a1' => null, 'b2' => 4, 0 => 3]], null], + ]; + + return $data; + } + + + /** + * @return mixed[][] + */ + public static function provideVariableDataForFallbackNullHandling() + { + // fallback = 42; + $data = [ + [['$0', [null]], null], + + [['$0 + $0', [null]], 84], + [['$0 + $1', [null, 41]], 83], + [['$0 + $1', [41, null]], 83], + [['$a1 + $b2 + $0', ['a1' => null, 'b2' => 41, 0 => 13]], 42 + 54], + + [['$0 * $0', [null]], 42 * 42], + [['$0 * $1', [null, 41]], 42 * 41], + [['$0 * $1', [41, null]], 41 * 42], + [['$a1 * $b2 * $0', ['a1' => null, 'b2' => 41, 0 => 13]], 42 * 41 * 13], + + [['$0 / $0', [null]], 1], + [['$0 / $1', [null, 41]], 42 / 41], + [['$0 / $1', [41, null]], 41 / 42], + [['$a1 / $b2 / $0', ['a1' => null, 'b2' => 41, 0 => 13]], 42 / 41 / 13], + + [['$0 % $0', [null]], 0], + [['$0 % $1', [null, 41]], 1], + [['$0 % $1', [41, null]], 41], + [['$a1 % $b2 % $0', ['a1' => null, 'b2' => 41, 0 => 13]], 42 % 41 % 13], + + [['$0 - $0', [null]], 0], + [['$0 - $1', [null, 41]], 1], + [['$0 - $1', [41, null]], -1], + [['$a1 - $b2 - $0', ['a1' => null, 'b2' => 41, 0 => 13]], 42 - 41 - 13], + + [['$0 ^ $0', [null]], 42 ** 42], + [['$0 ^ $1', [null, 41]], 42 ** 41], + [['$0 ^ $1', [41, null]], 41 ** 42], + [['$a1 ^ $b2 ^ $0', ['a1' => null, 'b2' => 4, 0 => 3]], 42 ** 4 ** 3], + ]; + + return $data; + } + + /** + * @return mixed[][] + */ + public static function provideVariableDataForLooseNullHandling() + { + // fallback = 42; + $data = [ + [['$0', [null]], null], + + [['$0 + $0', [null]], 0], + [['$0 + $1', [null, 41]], 41], + [['$0 + $1', [41, null]], 41], + [['$a1 + $b2 + $0', ['a1' => null, 'b2' => 41, 0 => 13]], 54], + + [['$0 * $0', [null]], 0], + [['$0 * $1', [null, 41]], 0], + [['$0 * $1', [41, null]], 0], + [['$a1 * $b2 * $0', ['a1' => null, 'b2' => 41, 0 => 13]], 0], + + [['$0 / $0', [null]], null], + [['$0 / $1', [null, 41]], 0], + [['$0 / $1', [41, null]], null], + [['$a1 / $b2 / $0', ['a1' => null, 'b2' => 41, 0 => 13]], 0], + + [['$0 % $0', [null]], null], + [['$0 % $1', [null, 41]], 0], + [['$0 % $1', [41, null]], null], + [['$a1 % $b2 % $0', ['a1' => null, 'b2' => 41, 0 => 13]], 0 % 41 % 13], + + [['$0 - $0', [null]], 0], + [['$0 - $1', [null, 41]], -41], + [['$0 - $1', [41, null]], 41], + [['$a1 - $b2 - $0', ['a1' => null, 'b2' => 41, 0 => 13]], -41 - 13], + + // 64 bit to the rescue ! + [['$0 ^ $0', [null]], 1], + [['$0 ^ $1', [null, 41]], 0 ** 41], + [['$0 ^ $1', [41, null]], 41 ** 0], + [['$a1 ^ $b2 ^ $0', ['a1' => null, 'b2' => 4, 0 => 3]], 0 ** 4 ** 3], + ]; + + return $data; + } + + /** + * @return mixed[][] + */ + public static function provideVariableDataForSkipHandling() + { + $data = [ + [['$0', [null]], null], + + [['$0 + $0', [null]], null], + [['$0 + $1', [null, 41]], 41], + [['$0 * $1', [41, null]], 41], + [['$a1 + $b2 + $0', ['a1' => null, 'b2' => 41, 0 => 13]], 54], + + [['$0 * $0', [null]], null], + [['$0 * $1', [null, 41]], 41], + [['$0 * $1', [41, null]], 41], + [['$a1 * $b2 * $0', ['a1' => null, 'b2' => 41, 0 => 13]], 41 * 13], + + [['$0 / $0', [null]], null], + [['$0 / $1', [null, 41]], 41], + [['$0 / $1', [41, null]], 41], + [['$a1 / $b2 / $0', ['a1' => null, 'b2' => 41, 0 => 13]], 41 / 13], + + [['$0 % $0', [null]], null], + [['$0 % $1', [null, 41]], 41], + [['$0 % $1', [41, null]], 41], + [['$a1 % $b2 % $0', ['a1' => null, 'b2' => 41, 0 => 13]], 41 % 13], + + [['$0 - $0', [null]], null], + [['$0 - $1', [null, 41]], 41], + [['$0 - $1', [41, null]], 41], + [['$a1 - $b2 - $0', ['a1' => null, 'b2' => 41, 0 => 13]], 41 - 13], + + [['$0 ^ $0', [null]], null], + [['$0 ^ $1', [null, 41]], 41], + [['$0 ^ $1', [41, null]], 41], + [['$a1 ^ $b2 ^ $0', ['a1' => null, 'b2' => 4, 0 => 3]], 4 ** 3], + ]; + + return $data; + } + /** * @return mixed[][] */ @@ -150,8 +327,7 @@ public static function provideInvalidVariableData() */ public function testValid($input, $expected) { - $mathParser = new Math(); - $this->assertSame($expected, $mathParser->evaluate($input)); + $this->assertSame($expected, Math::evaluate($input)); } /** @@ -159,9 +335,8 @@ public function testValid($input, $expected) */ public function testInvalidSyntax($input, $expected) { - $this->expectException(\RuntimeException::class); - $mathParser = new Math(); - $mathParser->evaluate($input); + $this->expectException(InvalidSyntaxException::class); + Math::evaluate($input); } /** @@ -169,9 +344,8 @@ public function testInvalidSyntax($input, $expected) */ public function testInvalidSyntaxType($input, $expected) { - $this->expectException(\RuntimeException::class); - $mathParser = new Math(); - $mathParser->evaluate($input); + $this->expectException(\TypeError::class); + Math::evaluate($input); } /** @@ -179,9 +353,7 @@ public function testInvalidSyntaxType($input, $expected) */ public function testVariables($input, $expected) { - $mathParser = new Math(); - $mathParser->setVariables($input[1]); - $this->assertSame($expected, $mathParser->evaluate($input[0])); + $this->assertSame($expected, Math::evaluate($input[0], $input[1])); } /** @@ -189,10 +361,8 @@ public function testVariables($input, $expected) */ public function testVariablesMultipleTimes($input, $expected) { - $mathParser = new Math(); - $mathParser->setVariables($input[1]); - $this->assertSame($expected, $mathParser->evaluate($input[0])); - $this->assertSame($expected, $mathParser->evaluate($input[0])); + $this->assertSame($expected, Math::evaluate($input[0], $input[1])); + $this->assertSame($expected, Math::evaluate($input[0], $input[1])); } /** @@ -201,9 +371,7 @@ public function testVariablesMultipleTimes($input, $expected) public function testInvalidVariable($input, $expected) { $this->expectException(\InvalidArgumentException::class); - $mathParser = new Math(); - $mathParser->setVariables($input[1]); - $mathParser->evaluate($input[0]); + Math::evaluate($input[0], $input[1]); } /** @@ -212,9 +380,7 @@ public function testInvalidVariable($input, $expected) public function testInvalidVariableWithRegister($input, $expected) { $this->expectException(\InvalidArgumentException::class); - $mathParser = new Math(); - $mathParser->registerVariable(0, $input[1][0]); - $mathParser->evaluate($input[0]); + Math::evaluate($input[0], [0 => $input[1][0]]); } /** @@ -222,9 +388,44 @@ public function testInvalidVariableWithRegister($input, $expected) */ public function testDistinctVariables($input, $expected) { - $mathParser = new Math(); - $stack = $mathParser->parse($input[0]); - $vars = $mathParser->getDistinctVariables($stack); + $stack = Math::parse($input[0]); + $vars = Math::getDistinctVariables($stack); $this->assertSame($expected, $vars); } + + /** + * @dataProvider provideVariableDataForStrictNullHandling + */ + public function testStrictNullHandling($input, $expected) + { + $value = Math::evaluate($input[0], $input[1]); + $this->assertSame($expected, $value); + } + + /** + * @dataProvider provideVariableDataForLooseNullHandling + */ + public function testLooseNullHandling($input, $expected) + { + $value = Math::evaluate($input[0], $input[1], ['null_handling' => 'loose']); + $this->assertSame($expected, $value); + } + + /** + * @dataProvider provideVariableDataForSkipHandling + */ + public function testSkipNullHandling($input, $expected) + { + $value = Math::evaluate($input[0], $input[1], ['null_handling' => 'skip']); + $this->assertSame($expected, $value); + } + + /** + * @dataProvider provideVariableDataForFallbackNullHandling + */ + public function testFallbackNullHandling($input, $expected) + { + $value = Math::evaluate($input[0], $input[1], ['null_handling' => 'fallback', 'fallback' => 42]); + $this->assertSame($expected, $value); + } } \ No newline at end of file