Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Lexer/Parser for mathematical expressions. (Input Processor + Separate Action & Condition) #183

Open
wants to merge 12 commits into
base: 8.x-3.x
Choose a base branch
from
127 changes: 127 additions & 0 deletions src/Math/Calculator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Calculator.
*/

namespace Drupal\rules\Math;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs @file


use Drupal\rules\Math\Exception\IncorrectExpressionException;
use Drupal\rules\Math\Exception\UnknownVariableException;
use Drupal\rules\Math\Token\FunctionToken;
use Drupal\rules\Math\Token\NumberToken;
use Drupal\rules\Math\Token\OperatorTokenInterface;
use Drupal\rules\Math\Token\VariableToken;

/**
* Parser for mathematical expressions.
*/
class Calculator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a docblock


/**
* Static cache of token streams in reverse polish (postfix) notation.
*
* @var array
*/
protected $tokenCache = [];

/**
* Constructs a new Calculator object.
*/
public function __construct() {
$this->lexer = new Lexer();

$this->lexer
->addOperator('plus', '\+', 'Drupal\rules\Math\Token\PlusToken')
->addOperator('minus', '\-', 'Drupal\rules\Math\Token\MinusToken')
->addOperator('multiply', '\*', 'Drupal\rules\Math\Token\MultiplyToken')
->addOperator('division', '\/', 'Drupal\rules\Math\Token\DivisionToken')
->addOperator('modulus', '\%', 'Drupal\rules\Math\Token\ModulusToken')
->addOperator('power', '\^', 'Drupal\rules\Math\Token\PowerToken');

$this->lexer
->addFunction('abs', 'abs', 1)
->addFunction('acos', 'acos', 1)
->addFunction('acosh', 'acosh', 1)
->addFunction('asin', 'asin', 1)
->addFunction('asinh', 'asinh', 1)
->addFunction('atan2', 'atan2', 2)
->addFunction('atan', 'atan', 1)
->addFunction('atanh', 'atanh', 1)
->addFunction('ceil', 'ceil', 1)
->addFunction('cos', 'cos', 1)
->addFunction('cosh', 'cosh', 1)
->addFunction('deg2rad', 'deg2rad', 1)
->addFunction('exp', 'exp', 1)
->addFunction('floor', 'floor', 1)
->addFunction('hypot', 'hypot', 2)
->addFunction('log10', 'log10', 1)
->addFunction('log', 'log', 2)
->addFunction('max', 'max', 2)
->addFunction('min', 'min', 2)
->addFunction('pow', 'pow', 2)
->addFunction('rad2deg', 'rad2deg', 1)
->addFunction('rand', 'rand', 2)
->addFunction('round', 'round', 1)
->addFunction('sin', 'sin', 1)
->addFunction('sinh', 'sinh', 1)
->addFunction('sqrt', 'sqrt', 1)
->addFunction('tan', 'tan', 1)
->addFunction('tanh', 'tanh', 1);

$this->lexer
->addConstant('pi', pi())
->addConstant('e', exp(1));
}

/**
* Calculates the result of a mathematical expression.
*
* @param string $expression
* The mathematical expression.
* @param array $variables
* A list of numerical values keyed by their variable names.
*
* @return mixed
* The result of the mathematical expression.
*
* @throws \Drupal\rules\Math\Exception\IncorrectExpressionException
* @throws \Drupal\rules\Math\Exception\IncorrectParenthesisException
*/
public function calculate($expression, $variables) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document this

$hash = md5($expression);
if (isset($this->tokenCache[$hash])) {
return $this->tokenCache[$hash];
}

$stream = $this->lexer->tokenize($expression);
$tokens = $this->lexer->postfix($stream);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after the postfix, tokens will be in reverse polish notation. Which is the reason why you can use a stack to evaluate. Would be cool to explain this a bit more

$this->tokenCache[$hash] = $tokens;

$stack = [];
foreach ($tokens as $token) {
if ($token instanceof NumberToken) {
array_push($stack, $token);
}
elseif ($token instanceof VariableToken) {
$identifier = $token->getValue();
if (!isset($variables[$identifier])) {
throw new UnknownVariableException($token->getOffset(), $identifier);
}
array_push($stack, new NumberToken($token->getOffset(), $variables[$identifier]));
}
elseif ($token instanceof OperatorTokenInterface || $token instanceof FunctionToken) {
array_push($stack, $token->execute($stack));
}
}

$result = array_pop($stack);
if (!empty($stack)) {
throw new IncorrectExpressionException();
}

return $result->getValue();
}

}
14 changes: 14 additions & 0 deletions src/Math/Exception/IncorrectExpressionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\IncorrectExpressionException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class IncorrectExpressionException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/IncorrectParenthesisException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\IncorrectParenthesisException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class IncorrectParenthesisException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/UnknownConstantException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\UnknownConstantException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class UnknownConstantException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/UnknownFunctionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\UnknownFunctionException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class UnknownFunctionException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/UnknownOperatorException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\UnknownOperatorException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class UnknownOperatorException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/UnknownTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\UnknownTokenException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class UnknownTokenException extends RulesException {

}
14 changes: 14 additions & 0 deletions src/Math/Exception/UnknownVariableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @file
* Contains \Drupal\rules\Math\Exception\UnknownVariableException.
*/

namespace Drupal\rules\Math\Exception;

use Drupal\rules\Exception\RulesException;

class UnknownVariableException extends RulesException {

}
Loading