j84reginato/my-eval

View on GitHub
src/Parsing/Nodes/Node.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace MyEval\Parsing\Nodes;

use MyEval\Exceptions\UnknownOperatorException;
use MyEval\Lexing\Token;
use MyEval\Lexing\TokenType;
use MyEval\Parsing\Nodes\Operand\BooleanNode;
use MyEval\Parsing\Nodes\Operand\ConstantNode;
use MyEval\Parsing\Nodes\Operand\FloatNode;
use MyEval\Parsing\Nodes\Operand\IntegerNode;
use MyEval\Parsing\Nodes\Operand\RationalNode;
use MyEval\Parsing\Nodes\Operand\VariableNode;
use MyEval\Parsing\Nodes\Operator\CloseBraceNode;
use MyEval\Parsing\Nodes\Operator\CloseParenthesisNode;
use MyEval\Parsing\Nodes\Operator\FunctionNode;
use MyEval\Parsing\Nodes\Operator\InfixExpressionNode;
use MyEval\Parsing\Nodes\Operator\OpenBraceNode;
use MyEval\Parsing\Nodes\Operator\OpenParenthesisNode;
use MyEval\Parsing\Nodes\Operator\PostfixExpressionNode;
use MyEval\Parsing\Nodes\Operator\TerminatorNode;
use MyEval\Parsing\Nodes\Operator\TernaryExpressionNode;
use MyEval\Parsing\Nodes\Operator\UnmappedNode;
use MyEval\Solving\ASCIIPrinter;
use MyEval\Solving\StdMathEvaluator;

/**
 * Abstract base class for nodes in the abstract syntax tree generated by the Parser (and some AST transformers).
 */
abstract class Node implements Visitable
{
    public const NUMERIC_INTEGER   = 1;
    public const NUMERIC_RATIONAL  = 2;
    public const NUMERIC_FLOAT     = 3;

    /**
     * Node factory, creating an appropriate Node from a Token.
     *
     * @param Token $token Provided token.
     *
     * @return Node
     * @throws UnknownOperatorException
     */
    public static function factory(Token $token): Node
    {
        return match ($token->type) {
            // Operands
            TokenType::NATURAL_NUMBER,
            TokenType::INTEGER                 => new IntegerNode((int)$token->value),
            TokenType::REAL_NUMBER             => new FloatNode((float)(str_replace(',', '.', $token->value))),
            TokenType::BOOLEAN                 => new BooleanNode($token->value),
            TokenType::VARIABLE                => new VariableNode($token->value),
            TokenType::CONSTANT                => new ConstantNode($token->value),

            // Operators
            TokenType::ADDITION_OPERATOR,
            TokenType::SUBTRACTION_OPERATOR,
            TokenType::MULTIPLICATION_OPERATOR,
            TokenType::DIVISION_OPERATOR,
            TokenType::EXPONENTIAL_OPERATOR,
            TokenType::EQUAL_TO,
            TokenType::GREATER_THAN,
            TokenType::LESS_THAN,
            TokenType::DIFFERENT_THAN,
            TokenType::GREATER_OR_EQUAL_THAN,
            TokenType::LESS_OR_EQUAL_THAN,
            TokenType::AND,
            TokenType::OR,
            TokenType::NOT                     => new InfixExpressionNode($token->value),

            TokenType::FACTORIAL_OPERATOR,
            TokenType::SEMI_FACTORIAL_OPERATOR => new PostfixExpressionNode($token->value),

            TokenType::IF                      => new TernaryExpressionNode(),

            TokenType::FUNCTION_NAME           => new FunctionNode($token->value),

            TokenType::OPEN_PARENTHESIS        => new OpenParenthesisNode($token->value),
            TokenType::CLOSE_PARENTHESIS       => new CloseParenthesisNode(),

            TokenType::OPEN_BRACE              => new OpenBraceNode($token->value),
            TokenType::CLOSE_BRACE             => new CloseBraceNode(),

            TokenType::THEN,
            TokenType::ELSE,
            TokenType::TERMINATOR              => new TerminatorNode($token->value),

            default                            => new UnmappedNode($token->value),
        };
    }

    /**
     * Convenience function for evaluating a tree, using the StdMathEvaluator class.
     *
     * ## Example usage:
     *
     * ~~~{.php}
     * use MyEval\Parsing\Parse;
     *
     * $parser = new Parser();
     * $node = $parser->parse('sin(x)cos(y)');
     * $functionValue = $node->evaluate( [ 'x' => 1.3, 'y' => 1.4 ] );
     * ~~~
     *
     * @param array $variables key-value array of variable values.
     *
     * @return float
     */
    public function evaluate(array $variables = []): float
    {
        $evaluator = new StdMathEvaluator($variables);

        return $this->accept($evaluator);
    }

    /**
     * Helper function, comparing two ASTs.
     *
     * Useful for testing and also for some AST transformers.
     *
     * @param Node $other Compare to this tree.
     *
     * @return bool
     */
    abstract public function compareTo(Node $other): bool;

    /**
     * Rough estimate of the complexity of the AST.
     *
     * Gives a rough measure of the complexity of an AST. This can be useful to choose between different simplification
     * rules or how to print a tree ("e^{...}" or ("\exp(...)") for example.
     *
     * More precisely, the complexity is computed as the sum of the complexity of all nodes of the AST.
     * - NumberNodes, VariableNodes and ConstantNodes have complexity 1,
     * - FunctionNodes have complexity 5 (plus the complexity of its operand),
     * - ExpressionNodes have complexity 1 (for `+`, `-`, `*`), 2 (for `/`), or 8 (for `^`)
     */
    public function complexity(): int
    {
        switch (\get_class($this)) {
            case IntegerNode::class:
            case VariableNode::class:
            case ConstantNode::class:
                $complexity = 1;
                break;
            case RationalNode::class:
            case FloatNode::class:
                $complexity = 2;
                break;
            case FunctionNode::class:
                /** @var FunctionNode $this */
                $complexity = 5 + $this->operand->complexity();
                break;
            case InfixExpressionNode::class:
                /** @var InfixExpressionNode $this */
                $operator = $this->operator;
                $left     = $this->getLeft();
                $right    = $this->getRight();
                return match ($operator) {
                    '+', '-', '*' => 1 + $left?->complexity() + (($right === null) ? 0 : $right->complexity()),
                    '/'           => 3 + $left?->complexity() + (($right === null) ? 0 : $right->complexity()),
                    '^'           => 8 + $left?->complexity() + (($right === null) ? 0 : $right->complexity()),
                };
                default:
                    // This shouldn't happen under normal circumstances.
                    $complexity = 1000;
                    break;
        }

        return $complexity;
    }

    /**
     * String representation of a Node.
     *
     * @return string
     */
    public function __toString(): string
    {
        $printer = new ASCIIPrinter();

        return $this->accept($printer);
    }
}