j84reginato/my-eval

View on GitHub
src/Solving/ASCIIPrinter.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace MyEval\Solving;

use MyEval\Exceptions\UnknownConstantException;
use MyEval\Exceptions\UnknownOperatorException;
use MyEval\Parsing\Nodes\Node;
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\FunctionNode;
use MyEval\Parsing\Nodes\Operator\InfixExpressionNode;
use MyEval\Parsing\Nodes\Operator\TernaryExpressionNode;

/**
 * Pretty-printing ASCII mathematical expression.
 *
 * Implementation of a Visitor, transforming an AST into ASCII string for the expression.
 *
 * ## Example:
 *
 * ~~~{.php}
 * use MyEval\StdMathEval;
 * use MyEval\Solving\ASCIIPrinter;
 *
 * $parser = new StdMathEval();
 * $tree = $parser->parse('exp(2x)+xy');
 * printer = new ASCIIPrinter();
 * result = $tree->accept($printer);  // Generates "exp(2*x)+x*y"
 * ~~~
 *
 * or more complex use:
 *
 * ~~~{.php}
 * use MyEval\Lexing\StdMathLexer;
 * use MyEval\Parsing\Parser;
 * use MyEval\Solving\ASCIIPrinter;
 *
 * // Tokenize
 * $lexer = new StdMathLexer();
 * $tokens = $lexer->tokenize('exp(2x)+xy');
 *
 * // Parse
 * $parser = new Parser();
 * $ast = $parser->parse($tokens);
 *
 * // Print
 * $printer = new ASCIIPrinter();
 * result = $tree->accept($printer);  // Generates "exp(2*x)+x*y"
 * ~~~
 */
class ASCIIPrinter implements Visitor, LogicVisitor
{
    /**
     * Generate ASCII output code for an IntegerNode.
     *
     * @param IntegerNode $node AST to be evaluated.
     */
    public function visitIntegerNode(IntegerNode $node): string
    {
        return (string)$node->value;
    }

    /**
     * Generate ASCII output code for a RationalNode.
     *
     * @param RationalNode $node AST to be evaluated.
     */
    public function visitRationalNode(RationalNode $node): string
    {
        $p = $node->getNumerator();
        $q = $node->getDenominator();

        if ($q === 1) {
            return (string)$p;
        }

        return "$p/$q";
    }

    /**
     * Generate ASCII output code for a NumberNode.
     *
     * @param FloatNode $node AST to be evaluated.
     */
    public function visitNumberNode(FloatNode $node): string
    {
        return (string)$node->value;
    }

    /**
     * Generate ASCII output code for a BooleanNode.
     *
     * @param BooleanNode $node AST to be evaluated.
     */
    public function visitBooleanNode(BooleanNode $node): string
    {
        return $node->value ? 'TRUE' : 'FALSE';
    }

    /**
     * Generate ASCII output code for a VariableNode.
     *
     * @param VariableNode $node AST to be evaluated.
     */
    public function visitVariableNode(VariableNode $node): string
    {
        return $node->value;
    }

    /**
     * Generate ASCII output code for a ConstantNode.
     *
     * @param ConstantNode $node AST to be evaluated.
     *
     * @return string
     * @throws UnknownConstantException
     */
    public function visitConstantNode(ConstantNode $node): string
    {
        return match ($node->value) {
            'pi'    => 'pi',
            'e'     => 'e',
            'i'     => 'i',
            'NAN'   => 'NAN',
            'INF'   => 'INF',
            default => throw new UnknownConstantException($node->value),
        };
    }

    /**
     * Generate ASCII output code for an ExpressionNode.
     *
     * Create a string giving ASCII output representing an ExpressionNode `(x op y)`,
     * where `op` is one of `+`, `-`, `*`, `/`, `^`, `=`, `>`, `<`, `<>`, `>=` or `<=`.
     *
     * @param InfixExpressionNode $node AST to be evaluated.
     *
     * @return string
     * @throws UnknownOperatorException
     */
    public function visitInfixExpressionNode(InfixExpressionNode $node): string
    {
        $left     = $node->getLeft();
        $operator = $node->operator;
        $right    = $node->getRight();

        switch ($operator) {
            case '+':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue+$rightValue";

            case '-':
            case '~':
                if ($right === null) {
                    // Unary minus
                    $leftValue = $this->parenthesize($left, $node);
                    return "-$leftValue";
                }
                // Binary minus
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue-$rightValue";

            case '*':
            case '/':
                $leftValue  = $this->parenthesize($left, $node);
                $rightValue = $this->parenthesize($right, $node, '', true);
                return "$leftValue$operator$rightValue";

            case '^':
                $leftValue  = $this->parenthesize($left, $node, '', true);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue$operator$rightValue";

            case '=':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue=$rightValue";

            case '>':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue>$rightValue";

            case '<':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue<$rightValue";

            case '<>':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue<>$rightValue";

            case '>=':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue>=$rightValue";

            case '<=':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue<=$rightValue";

            case 'AND':
            case '&&':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue AND $rightValue";

            case 'OR':
            case '||':
                $leftValue  = $left?->accept($this);
                $rightValue = $this->parenthesize($right, $node);
                return "$leftValue OR $rightValue";

            default:
                throw new UnknownOperatorException($operator);
        }
    }

    /**
     * Generate ASCII output code for an IfNode.
     *
     * @param TernaryExpressionNode $node AST to be evaluated.
     *
     * @return string
     */
    public function visitTernaryNode(TernaryExpressionNode $node): string
    {
        if ($node->getCondition() && $node->getLeft() && $node->getRight()) {
            return $node->operator . ' (' . $node->getCondition() . ') {'
                . $node->getLeft() . '} else {' . $node->getRight() . '}';
        }

        return $node->operator;
    }

    /**
     * Generate ASCII output code for an FunctionNode.
     *
     * @param FunctionNode $node AST to be evaluated.
     *
     * @return string
     */
    public function visitFunctionNode(FunctionNode $node): string
    {
        $functionName = $node->operator;

        if ($functionName === '!' || $functionName === '!!') {
            return $this->visitFactorialNode($node);
        }

        if ($node->operand === null) {
            return '';
        }
        $operand = $node->operand->accept($this);

        return "$functionName($operand)";
    }

    /**
     * Generate ASCII output code for a factorial FunctionNode.
     *
     * @param FunctionNode $node
     *
     * @return string
     */
    private function visitFactorialNode(FunctionNode $node): string
    {
        $functionName = $node->operator;
        $operand      = $node->operand;
        $op           = $operand->accept($this);

        // Add parentheses most of the time.
        if ($operand instanceof FloatNode || $operand instanceof IntegerNode || $operand instanceof RationalNode) {
            if ($operand->value < 0) {
                $op = "($op)";
            }
        } elseif (!$operand instanceof VariableNode && !$operand instanceof ConstantNode) {
            $op = "($op)";
        }

        return "$op$functionName";
    }

    /**
     * @param Node|null           $node
     * @param InfixExpressionNode $cutoff
     * @param string              $prepend
     * @param bool                $conservative
     *
     * @return string
     * @throws UnknownOperatorException
     */
    public function parenthesize(
        ?Node $node,
        InfixExpressionNode $cutoff,
        string $prepend = '',
        bool $conservative = false
    ): string {
        $text = $node?->accept($this);

        if ($node instanceof InfixExpressionNode) {
            // Second term is a unary minus
            if ($node->operator === '-' && $node->getRight() === null) {
                return "($text)";
            }

            if ($cutoff->operator === '-' && $node->lowerPrecedenceThan($cutoff)) {
                return "($text)";
            }

            if ($conservative) {
                // Add parentheses more liberally for / and ^ operators, so that e.g. x/(y*z) is printed correctly
                if ($cutoff->operator === '/' && $node->lowerPrecedenceThan($cutoff)) {
                    return "($text)";
                }
                if ($cutoff->operator === '^' && $node->operator === '^') {
                    return "($text)";
                }
            }

            if ($node->strictlyLowerPrecedenceThan($cutoff)) {
                return "($text)";
            }
        }

        if (
            ($node instanceof FloatNode || $node instanceof IntegerNode || $node instanceof RationalNode) &&
            $node->value < 0
        ) {
            return "($text)";
        }

        // Treat rational numbers as divisions on printing
        if ($node instanceof RationalNode && $node->getDenominator() !== 1) {
            $fakeNode = new InfixExpressionNode('/', $node->getNumerator(), $node->getDenominator());

            if ($fakeNode->lowerPrecedenceThan($cutoff)) {
                return "($text)";
            }
        }

        return "$prepend$text";
    }
}