j84reginato/my-eval

View on GitHub
src/Solving/LogicEvaluator.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace MyEval\Solving;

use MyEval\Exceptions\NullOperandException;
use MyEval\Exceptions\UnknownOperatorException;
use MyEval\Parsing\Nodes\Operand\BooleanNode;
use MyEval\Parsing\Nodes\Operator\InfixExpressionNode;
use MyEval\Parsing\Nodes\Operator\TernaryExpressionNode;

/**
 * Evaluate a logical parsed expression.
 *
 * Implementation of a Visitor, transforming an AST into a floating point number, giving the *value* of the expression
 * represented by the AST.
 *
 * The class implements evaluation of all arithmetic operators as well as every elementary function and predefined
 * constant recognized by StdMathLexer and LogicLexer.
 *
 * ## Example:
 *
 * ~~~{.php}
 * use MyEval\LogicEval;
 *
 * $evaluator = new LogicEval();
 * $result = $evaluator->evaluate('if (x > y) { 9; } else { 8; }', [ 'x' => 5, 'y' => 4 ]);  // Results 9.
 * ~~~
 *
 * or more complex use:
 *
 * ~~~{.php}
 * use MyEval\Lexing\LogicLexer;
 * use MyEval\Parsing\Parser;
 * use MyEval\Solving\LogicEvaluator;
 *
 * // Tokenize
 * $lexer = new LogicLexer();
 * $tokens = $lexer->tokenize('if (x > y) { 9; } else { 8; }');
 *
 * // Parse
 * $parser = new Parser();
 * $ast = $parser->parse($tokens);
 *
 * // Evaluate
 * $evaluator = new LogicEvaluator([ 'x' => 5, 'y' => 4 ]);
 * $value = $ast->accept($evaluator);
 * ~~~
 */
class LogicEvaluator extends StdMathEvaluator implements LogicVisitor
{
    /**
     * Evaluate a BooleanNode.
     *
     * @param BooleanNode $node AST to be evaluated.
     *
     * @return bool
     */
    public function visitBooleanNode(BooleanNode $node): bool
    {
        return $node->value;
    }

    /**
     * Evaluate an Logical ExpressionNode.
     *
     * Computes the value of an ExpressionNode `x op y` where `op` is one of `=`, `>`, `<`, `<>`, `>=`, `<=`,
     * `&&`, `||`, `AND` or `OR`.
     *
     * @param InfixExpressionNode $node AST to be evaluated.
     *
     * @return bool
     * @throws UnknownOperatorException
     * @throws NullOperandException
     */
    public function visitLogicalExpressionNode(InfixExpressionNode $node): bool
    {
        $left     = $node->getLeft();
        $operator = $node->operator;
        $right    = $node->getRight();

        if ($left === null || ($right === null)) {
            throw new NullOperandException();
        }

        $rightValue = $left->accept($this);
        $leftValue  = $right->accept($this);

        // Perform the right operation based on the operator
        return match ($operator) {
            '='         => $rightValue == $leftValue,
            '<>'        => $rightValue != $leftValue,
            '>'         => $rightValue > $leftValue,
            '<'         => $rightValue < $leftValue,
            '>='        => $rightValue >= $leftValue,
            '<='        => $rightValue <= $leftValue,
            '&&', 'AND' => $rightValue && $leftValue,
            '||', 'OR'  => $rightValue || $leftValue,
            default     => throw new UnknownOperatorException($operator),
        };
    }

    /**
     * Evaluate an visitTernaryNode.
     *
     * Computes the value of an visitTernaryNode `x op y` where `op` is one of `=`, `>`, `<`, `<>`, `<=`or `<=`.
     *
     * @param TernaryExpressionNode $node AST to be evaluated.
     *
     * @return float
     * @throws NullOperandException
     */
    public function visitTernaryNode(TernaryExpressionNode $node): float
    {
        /** @var InfixExpressionNode|BooleanNode $condition */
        $condition = $node->getCondition();

        $isBooleanNode = $condition instanceof BooleanNode;

        $left  = $isBooleanNode ? $node->getLeft() : $condition->getLeft();
        $right = $isBooleanNode ? $node->getRight() : $condition->getRight();

        if ($left === null || ($right === null)) {
            throw new NullOperandException();
        }

        $leftOperand  = $left->accept($this);
        $rightOperand = $right->accept($this);
        $operator     = $isBooleanNode ? $node->operator : $condition->operator;

        if ($isBooleanNode) {
            return $condition->value ? $leftOperand : $rightOperand;
        }

        switch ($operator) {
            default:
            case '=':
                $boolValue = $leftOperand == $rightOperand;
                break;
            case '>':
                $boolValue = $leftOperand > $rightOperand;
                break;
            case '<':
                $boolValue = $leftOperand < $rightOperand;
                break;
            case '<>':
                $boolValue = $leftOperand != $rightOperand;
                break;
            case '>=':
                $boolValue = $leftOperand >= $rightOperand;
                break;
            case '<=':
                $boolValue = $leftOperand <= $rightOperand;
                break;
        }

        $leftValue  = (float)$node->getLeft()?->accept($this);
        $rightValue = (float)$node->getRight()?->accept($this);

        return $boolValue ? $leftValue : $rightValue;
    }
}