j84reginato/my-eval

View on GitHub
src/Solving/LaTeXPrinter.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace MyEval\Solving;

use MyEval\Exceptions\NullOperandException;
use MyEval\Exceptions\SyntaxErrorException;
use MyEval\Exceptions\UnknownConstantException;
use MyEval\Exceptions\UnknownOperatorException;
use MyEval\Parsing\Nodes\Node;
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;

/**
 * Create LaTeX output code for prettyprinting a mathematical expression (for example via MathJax)
 *
 * Implementation of a Visitor, transforming an AST into a string giving LaTeX code for the expression.
 *
 * The class in general does *not* generate the best possible LaTeX code, and needs more work to be used in a
 * production setting.
 *
 * ## Example:
 *
 * ~~~{.php}
 * use MyEval\StdMathEval;
 * use MyEval\Solving\LaTeXPrinter;
 *
 * $parser = new StdMathEval();
 * $ast = $parser->parse('exp(2x)+xy');
 * printer = new LaTeXPrinter();
 * result = $ast->accept($printer);  // Generates "e^{2x}+xy"
 * ~~~
 *
 * or more complex use:
 *
 * ~~~{.php}
 * use MyEval\Lexing\StdMathLexer;
 * use MyEval\Parsing\Parser;
 * use MyEval\Solving\LaTeXPrinter;
 *
 * // Tokenize
 * $lexer = new StdMathLexer();
 * $tokens = $lexer->tokenize('exp(2x)+xy');
 *
 * // Parse
 * $parser = new Parser();
 * $ast = $parser->parse($tokens);
 *
 * // Print
 * printer = new LaTeXPrinter();
 * result = $ast->accept($printer);  // Generates "e^{2x}+xy"
 * ~~~
 *
 * Note that surrounding `$`, `$$` or `\begin{equation}..\end{equation}` has to be added manually.
 */
class LaTeXPrinter implements Visitor
{
    /**
     * @var bool Flag to determine if division should be typeset with a solidus, e.g. x/y or a fraction \frac{x}{y}.
     */
    private bool $solidus = false;

    /**
     * Generate a LaTeX output code for an IntegerNode.
     *
     * @param IntegerNode $node AST to be typeset.
     *
     * @return string
     */
    public function visitIntegerNode(IntegerNode $node): string
    {
        return (string)$node->value;
    }

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

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

        if ($this->solidus) {
            return "$p/$q";
        }

        return "\\frac" . '{' . $p . '}{' . $q . '}';
    }

    /**
     * Generate LaTeX output code for a NumberNode.
     *
     * Create a string giving LaTeX code for a NumberNode. Currently, there is no special formatting of numbers.
     *
     * @param FloatNode $node AST to be typeset.
     *
     * @return string
     */
    public function visitNumberNode(FloatNode $node): string
    {
        return (string)$node->value;
    }

    /**
     * Evaluate a BooleanNode.
     *
     * @return bool
     * @throws SyntaxErrorException
     */
    public function visitBooleanNode(): bool
    {
        throw new SyntaxErrorException();
    }

    /**
     * Generate LaTeX output code for a VariableNode.
     *
     * Create a string giving LaTeX code for a VariableNode. Currently, there is no special formatting of variables.
     *
     * @param VariableNode $node AST to be typeset.
     *
     * @return string
     */
    public function visitVariableNode(VariableNode $node): string
    {
        return $node->value;
    }

    /**
     * Generate LaTeX output code for a ConstantNode.
     *
     * Create a string giving LaTeX code for a ConstantNode.
     * `pi` typesets as `\pi` and `e` simply as `e`.
     *
     * @param ConstantNode $node AST to be typeset.
     *
     * @return string
     * @throws UnknownConstantException for nodes representing other constants.
     */
    public function visitConstantNode(ConstantNode $node): string
    {
        return match ($node->value) {
            'pi'    => '\pi{}',
            'e'     => 'e',
            'i'     => 'i',
            'NAN'   => '\operatorname{NAN}',
            'INF'   => '\infty{}',
            default => throw new UnknownConstantException($node->value),
        };
    }

    /**
     * Generate LaTeX output code for an ExpressionNode.
     *
     * Create a string giving LaTeX code for an ExpressionNode `(x op y)` where `op` is `+`, `-`, `*`, `/` or `^`.
     *
     * ## Typesetting rules:
     *
     * - Adds parentheses around each operand, if needed. I.e. if their precedence lower than that of the current Node.
     *   For example, the AST `(^ (+ 1 2) 3)` generates `(1+2)^3` but `(+ (^ 1 2) 3)` generates `1^2+3` as expected.
     * - Multiplications are typeset implicitly `(* x y)` returns `xy` or using `\cdot` if the first factor is a
     *   FunctionNode or the (left operand) in the second factor is a NumberNode, so `(* x 2)` return `x \cdot 2` and
     *   `(* (sin x) x)` return `\sin x \cdot x` (but `(* x (sin x))` returns `x\sin x`).
     * - Divisions are typeset using `\frac`.
     * - Exponentiation adds braces around the power when needed.
     *
     * @param InfixExpressionNode $node AST to be typeset.
     *
     * @return string
     * @throws UnknownOperatorException
     * @throws NullOperandException
     */
    public function visitInfixExpressionNode(InfixExpressionNode $node): string
    {
        $left     = $node->getLeft();
        $operator = $node->operator;
        $right    = $node->getRight();

        if ($left === null || ($right === null && $operator !== '-')) {
            throw new NullOperandException();
        }

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

                return "$leftValue+$rightValue";

            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 '*':
                $operator = '';
                if ($this->multiplicationNeedsCdot($left, $right)) {
                    $operator = '\cdot ';
                }
                $leftValue  = $this->parenthesize($left, $node);
                $rightValue = $this->parenthesize($right, $node);

                return "$leftValue$operator$rightValue";

            case '/':
                if ($this->solidus) {
                    $leftValue  = $this->parenthesize($left, $node);
                    $rightValue = $this->parenthesize($right, $node);

                    return "$leftValue$operator$rightValue";
                }

                return '\frac{' . $left->accept($this) . '}{' . $right->accept($this) . '}';

            case '^':
                $leftValue = $this->parenthesize($left, $node, '', true);

                // Typeset exponents with solidus
                $this->solidus = true;
                $result        = $leftValue . '^' . $this->bracesNeeded($right);
                $this->solidus = false;

                return $result;

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

    /**
     * Evaluate a TernaryNode.
     *
     * @return void
     * @throws SyntaxErrorException
     */
    public function visitTernaryNode(): void
    {
        throw new SyntaxErrorException();
    }

    /**
     * Generate LaTeX output code for a FunctionNode.
     *
     * Create a string giving LaTeX code for a functionNode.
     *
     * ## Typesetting rules:
     *
     * - `sqrt(op)` is typeset as `\sqrt{op}.
     * - `exp(op)` is either typeset as `e^{op}`, if `op` is a simple expression or as `\exp(op)` for more complicated
     *   operands.
     *
     * @param FunctionNode $node AST to be typeset.
     *
     * @return string
     */
    public function visitFunctionNode(FunctionNode $node): string
    {
        $functionName = $node->operator;

        $operand = $node->operand->accept($this);

        switch ($functionName) {
            case 'sqrt':
                return "\\$functionName{" . $node->operand->accept($this) . '}';
            case 'exp':
                $operand = $node->operand;
                if ($operand->complexity() < 10) {
                    $this->solidus = true;
                    $result        = 'e^' . $this->bracesNeeded($operand);
                    $this->solidus = false;

                    return $result;
                }
                // Operand is complex, typset using \exp instead

                return '\exp(' . $operand->accept($this) . ')';

            case 'ln':
            case 'log':
            case 'sin':
            case 'cos':
            case 'tan':
            case 'arcsin':
            case 'arccos':
            case 'arctan':
                break;

            case 'abs':
                $operand = $node->operand;

                return '\lvert ' . $operand->accept($this) . '\rvert ';

            case '!':
            case '!!':
                return $this->visitFactorialNode($node);

            default:
                $functionName = 'operatorname{' . $functionName . '}';
        }

        return "\\$functionName($operand)";
    }

    /**
     * Check if a multiplication needs an inserted \cdot or if it can be safely written with implicit multiplication.
     *
     * @param Node $left  AST of first factor.
     * @param Node $right AST of second factor.
     *
     * @return bool
     */
    private function multiplicationNeedsCdot(Node $left, Node $right): bool
    {
        if ($left instanceof FunctionNode) {
            return true;
        }

        if ($this->isNumeric($right)) {
            return true;
        }

        if ($right instanceof InfixExpressionNode && $this->isNumeric($right->getLeft())) {
            return true;
        }

        return false;
    }

    /**
     * Generate LaTeX code for factorials.
     *
     * @param FunctionNode $node AST to be typeset.
     *
     * @return string
     */
    private function visitFactorialNode(FunctionNode $node): string
    {
        $functionName = $node->operator;
        $op           = $node->operand;
        $operand      = $op->accept($this);

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

        return "$operand$functionName";
    }

    /**
     *  Add parentheses to the LaTeX representation of $node if needed.
     *
     * @param Node                $node   The AST to typeset
     * @param InfixExpressionNode $cutoff A token representing the precedence of the parent node. Operands with a lower
     *                                    precedence have parentheses added.
     * @param string              $prepend
     * @param bool                $conservative
     *
     * @return string
     */
    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 ($node->strictlyLowerPrecedenceThan($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 ($this->isNumeric($node) && $node->value < 0) {
            return "($text)";
        }

        return "$prepend$text";
    }

    /**
     * Add curly braces around the LaTex representation of $node if needed.
     *
     * Nodes representing a single ConstantNode, VariableNode or NumberNodes (0--9) are returned as-is.
     * Other Nodes get curly braces around their LaTeX code.
     *
     * @param Node $node AST to parse.
     *
     * @return string
     */
    public function bracesNeeded(Node $node): string
    {
        if ($node instanceof VariableNode || $node instanceof ConstantNode) {
            return $node->accept($this);
        }

        if ($node instanceof IntegerNode && $node->value >= 0 && $node->value <= 9) {
            return $node->accept($this);
        }

        return '{' . $node->accept($this) . '}';
    }

    /**
     * Check if Node is numeric, i.e. a NumberNode, IntegerNode or RationalNode.
     *
     * @param Node $node AST to check.
     *
     * @return bool
     */
    private function isNumeric(Node $node): bool
    {
        return ($node instanceof FloatNode || $node instanceof IntegerNode || $node instanceof RationalNode);
    }
}