Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# A simple and extensible math expressions calculator

## Features:
* Built in support for +, -, *, / and power (^) operators
* Built in support for +, -, *, %, / and power (^) operators
* Paratheses () and arrays [] are fully supported
* Logical operators (==, !=, <, <, >=, <=, &&, ||)
* Built in support for most PHP math functions
Expand Down Expand Up @@ -101,7 +101,7 @@ $executor->calculate('avarage(1, 3, 4, 8)'); // 4
```

## Operators:
Default operators: `+ - * / ^`
Default operators: `+ - * / % ^`

Add custom operator to executor:

Expand All @@ -111,7 +111,7 @@ use NXP\Classes\Operator;
$executor->addOperator(new Operator(
'%', // Operator sign
false, // Is right associated operator
170, // Operator priority
180, // Operator priority
function (&$stack)
{
$op2 = array_pop($stack);
Expand Down Expand Up @@ -189,6 +189,10 @@ $calculator->setVarNotFoundHandler(
);
```

## Floating Point BCMath Support
By default, `MathExecutor` uses PHP floating point math, but if you need a fixed precision, call **useBCMath()**. Precision defaults to 2 decimal points, or pass the required number.
`WARNING`: Functions may return a PHP floating point number. By doing the basic math functions on the results, you will get back a fixed number of decimal points. Use a plus sign in from of any stand alone function to return the proper number of decimal places.

## Division By Zero Support:
Division by zero throws a `\NXP\Exception\DivisionByZeroException` by default
```php
Expand Down
44 changes: 41 additions & 3 deletions src/NXP/MathExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public function removeVars() : self
*
* @return array<Operator> of operator class names
*/
public function getOperators()
public function getOperators() : array
{
return $this->operators;
}
Expand All @@ -279,6 +279,18 @@ public function getFunctions() : array
return $this->functions;
}

/**
* Remove a specific operator
*
* @return array<Operator> of operator class names
*/
public function removeOperator(string $operator) : self
{
unset($this->operators[$operator]);

return $this;
}

/**
* Set division by zero returns zero instead of throwing DivisionByZeroException
*/
Expand All @@ -301,16 +313,39 @@ public function getCache() : array
/**
* Clear token's cache
*/
public function clearCache() : void
public function clearCache() : self
{
$this->cache = [];

return $this;
}

public function useBCMath(int $scale = 2) : self
{
\bcscale($scale);
$this->addOperator(new Operator('+', false, 170, static fn($a, $b) => \bcadd("{$a}", "{$b}")));
$this->addOperator(new Operator('-', false, 170, static fn($a, $b) => \bcsub("{$a}", "{$b}")));
$this->addOperator(new Operator('uNeg', false, 200, static fn($a) => \bcsub('0.0', "{$a}")));
$this->addOperator(new Operator('*', false, 180, static fn($a, $b) => \bcmul("{$a}", "{$b}")));
$this->addOperator(new Operator('/', false, 180, static function($a, $b) {
/** @todo PHP8: Use throw as expression -> static fn($a, $b) => 0 == $b ? throw new DivisionByZeroException() : $a / $b */
if (0 == $b) {
throw new DivisionByZeroException();
}

return \bcdiv("{$a}", "{$b}");
}));
$this->addOperator(new Operator('^', true, 220, static fn($a, $b) => \bcpow("{$a}", "{$b}")));
$this->addOperator(new Operator('%', false, 180, static fn($a, $b) => \bcmod("{$a}", "{$b}")));

return $this;
}

/**
* Set default operands and functions
* @throws ReflectionException
*/
protected function addDefaults() : void
protected function addDefaults() : self
{
foreach ($this->defaultOperators() as $name => $operator) {
[$callable, $priority, $isRightAssoc] = $operator;
Expand All @@ -323,6 +358,8 @@ protected function addDefaults() : void

$this->onVarValidation = [$this, 'defaultVarValidation'];
$this->variables = $this->defaultVars();

return $this;
}

/**
Expand Down Expand Up @@ -352,6 +389,7 @@ static function($a, $b) { /** @todo PHP8: Use throw as expression -> static fn($
false
],
'^' => [static fn($a, $b) => \pow($a, $b), 220, true],
'%' => [static fn($a, $b) => $a % $b, 180, false],
'&&' => [static fn($a, $b) => $a && $b, 100, false],
'||' => [static fn($a, $b) => $a || $b, 90, false],
'==' => [static fn($a, $b) => \is_string($a) || \is_string($b) ? 0 == \strcmp($a, $b) : $a == $b, 140, false],
Expand Down
242 changes: 241 additions & 1 deletion tests/MathTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public function providerExpressions()
['tanh(1.5)'],

['0.1 + 0.2'],
['0.1 + 0.2 - 0.3'],
['1 + 2'],

['0.1 - 0.2'],
Expand Down Expand Up @@ -246,7 +247,246 @@ public function providerExpressions()
['max(1,2,4.9,3)'],
['min(1,2,4.9,3)'],
['max([1,2,4.9,3])'],
['min([1,2,4.9,3])']
['min([1,2,4.9,3])'],

['4 % 4'],
['7 % 4'],
['99 % 4'],
['123 % 7'],
];
}

/**
* @dataProvider bcMathExpressions
*/
public function testBCMathCalculating(string $expression, string $expected = '') : void
{
$calculator = new MathExecutor();
$calculator->useBCMath();

if ('' === $expected)
{
$expected = $expression;
}

/** @var float $phpResult */
eval('$phpResult = ' . $expected . ';');

try {
$result = $calculator->execute($expression);
} catch (Exception $e) {
$this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', \get_class($e), $e->getFile(), $e->getLine(), $expression));
}
$this->assertEquals($phpResult, $result, "Expression was: {$expression}");
}

/**
* Expressions data provider
*
* Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval.
* The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
* something more complex and not a simple mathmatical expression.
*/
public function bcMathExpressions()
{
return [
['-5'],
['-5+10'],
['4-5'],
['4 -5'],
['(4*2)-5'],
['(4*2) - 5'],
['4*-5'],
['4 * -5'],
['+5'],
['+(3+2)'],
['+(+3+2)'],
['+(-3+2)'],
['-5'],
['-(-5)'],
['-(+5)'],
['+(-5)'],
['+(+5)'],
['-(3+2)'],
['-(-3+-2)'],

['abs(1.5)'],
['acos(0.15)'],
['acosh(1.5)'],
['asin(0.15)'],
['atan(0.15)'],
['atan2(1.5, 3.5)'],
['atanh(0.15)'],
['bindec("10101")'],
['ceil(1.5)'],
['cos(1.5)'],
['cosh(1.5)'],
['decbin("15")'],
['dechex("15")'],
['decoct("15")'],
['deg2rad(1.5)'],
['exp(1.5)'],
['expm1(1.5)'],
['floor(1.5)'],
['fmod(1.5, 3.5)'],
['hexdec("abcdef")'],
['hypot(1.5, 3.5)'],
['intdiv(10, 2)'],
['log(1.5)'],
['log10(1.5)'],
['log1p(1.5)'],
['max(1.5, 3.5)'],
['min(1.5, 3.5)'],
['octdec("15")'],
['pi()'],
['pow(1.5, 3.5)'],
['rad2deg(1.5)'],
['round(1.5)'],
['sin(1.5)'],
['sin(12)'],
['+sin(12)'],
['-sin(12)', '0.53'],
['sinh(1.5)'],
['sqrt(1.5)'],
['tan(1.5)'],
['tanh(1.5)'],

['0.1 + 0.2'],
['0.1 + 0.2 - 0.3'],
['1 + 2'],

['0.1 - 0.2'],
['1 - 2'],

['0.1 * 2'],
['1 * 2'],

['0.1 / 0.2'],
['1 / 2'],

['2 * 2 + 3 * 3'],
['2 * 2 / 3 * 3', '3.99'],
['2 / 2 / 3 / 3', '0.11'],
['2 / 2 * 3 / 3'],
['2 / 2 * 3 * 3'],

['1 + 0.6 - 3 * 2 / 50'],

['(5 + 3) * -1'],

['-2- 2*2'],
['2- 2*2'],
['2-(2*2)'],
['(2- 2)*2'],
['2 + 2*2'],
['2+ 2*2'],
['2+2*2'],
['(2+2)*2'],
['(2 + 2)*-2'],
['(2+-2)*2'],

['1 + 2 * 3 / (min(1, 5) + 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) - 2 + 5)'],
['1 + 2 * 3 / (min(1, 5) * 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 * 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 / 1)'],
['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)', '1.85'],
['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'],
['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)', '1.85'],
['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'],

['(1 + 2) * 3 / (3 / min(1, 5) / 2 + 1)'],

['sin(10) * cos(50) / min(10, 20/2)', '-0.05'],
['sin(10) * cos(50) / min(10, (20/2))', '-0.05'],
['sin(10) * cos(50) / min(10, (max(10,20)/2))', '-0.05'],

['1 + "2" / 3', '1.66'],
["1.5 + '2.5' / 4", '2.12'],
['1.5 + "2.5" * ".5"'],

['-1 + -2'],
['-1+-2'],
['-1- -2'],
['-1/-2'],
['-1*-2'],

['(1+2+3+4-5)*7/100'],
['(-1+2+3+4- 5)*7/100'],
['(1+2+3+4- 5)*7/100'],
['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'],

['1 && 0'],
['1 && 0 && 1'],
['1 || 0'],
['1 && 0 || 1'],

['5 == 3'],
['5 == 5'],
['5 != 3'],
['5 != 5'],
['5 > 3'],
['3 > 5'],
['3 >= 5'],
['3 >= 3'],
['3 < 5'],
['5 < 3'],
['3 <= 5'],
['5 <= 5'],
['10 < 9 || 4 > (2+1)'],
['10 < 9 || 4 > (-2+1)'],
['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'],

['1 + 5 == 3 + 1'],
['1 + 5 == 5 + 1'],
['1 + 5 != 3 + 1'],
['1 + 5 != 5 + 1'],
['1 + 5 > 3 + 1'],
['1 + 3 > 5 + 1'],
['1 + 3 >= 5 + 1'],
['1 + 3 >= 3 + 1'],
['1 + 3 < 5 + 1'],
['1 + 5 < 3 + 1'],
['1 + 3 <= 5 + 1'],
['1 + 5 <= 5 + 1'],

['(-4)'],
['(-4 + 5)'],
['(3 * 1)'],
['(-3 * -1)'],
['1 + (-3 * -1)'],
['1 + ( -3 * 1)'],
['1 + (3 *-1)'],
['1 - 0'],
['1-0'],

['-(1.5)'],
['-log(4)', '-1.38'],
['0-acosh(1.5)', '-0.96'],
['-acosh(1.5)', '-0.96'],
['-(-4)'],
['-(-4 + 5)'],
['-(3 * 1)'],
['-(-3 * -1)'],
['-1 + (-3 * -1)'],
['-1 + ( -3 * 1)'],
['-1 + (3 *-1)'],
['-1 - 0'],
['-1-0'],
['-(4*2)-5'],
['-(4*-2)-5'],
['-(-4*2) - 5'],
['-4*-5'],
['max(1,2,4.9,3)'],
['min(1,2,4.9,3)'],
['max([1,2,4.9,3])'],
['min([1,2,4.9,3])'],

['4 % 4'],
['7 % 4'],
['99 % 4'],
['123 % 7'],
];
}

Expand Down