package jnpf.elnFormula; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; public final class FormulaLatexRenderer { private final FormulaFunctionRegistry functions = new FormulaFunctionRegistry(); public String render(FormulaAst.Node node, Map parameterDisplayNames) { if (node == null || parameterDisplayNames == null) { throw new IllegalArgumentException("node and parameterDisplayNames must not be null"); } return renderNode(node, parameterDisplayNames, 0, false); } private String renderNode(FormulaAst.Node node, Map names, int parentPrecedence, boolean rightChild) { int precedence = precedence(node); String rendered; if (node instanceof FormulaAst.Literal) { rendered = literal((FormulaAst.Literal) node); } else if (node instanceof FormulaAst.Parameter) { FormulaAst.Parameter parameter = (FormulaAst.Parameter) node; String displayName = names.get(parameter.getCode()); rendered = "\\text{" + escapeText(displayName == null ? parameter.getCode() : displayName) + "}"; } else if (node instanceof FormulaAst.Unary) { FormulaAst.Unary unary = (FormulaAst.Unary) node; String operator = unaryOperator(unary.getOperator()); rendered = operator + renderNode(unary.getOperand(), names, precedence, true); } else if (node instanceof FormulaAst.Binary) { rendered = binary((FormulaAst.Binary) node, names); } else if (node instanceof FormulaAst.FunctionCall) { rendered = function((FormulaAst.FunctionCall) node, names); } else { throw new IllegalArgumentException("unsupported formula node"); } boolean parentheses = precedence < parentPrecedence || (rightChild && precedence == parentPrecedence && node instanceof FormulaAst.Binary); return parentheses ? "\\left(" + rendered + "\\right)" : rendered; } private String binary(FormulaAst.Binary binary, Map names) { String operator = binary.getOperator(); int precedence = precedence(binary); if ("/".equals(operator)) { return "\\frac{" + renderNode(binary.getLeft(), names, 0, false) + "}{" + renderNode(binary.getRight(), names, 0, false) + "}"; } if ("^".equals(operator)) { String left = renderNode(binary.getLeft(), names, precedence, false); if (binary.getLeft() instanceof FormulaAst.Binary && "^".equals(((FormulaAst.Binary) binary.getLeft()).getOperator())) { left = "\\left(" + left + "\\right)"; } return left + "^{" + renderNode(binary.getRight(), names, 0, false) + "}"; } String left = renderNode(binary.getLeft(), names, precedence, false); if (precedence == 3 && binary.getLeft() instanceof FormulaAst.Binary && precedence(binary.getLeft()) == precedence) { left = "\\left(" + left + "\\right)"; } return left + latexOperator(operator) + renderNode(binary.getRight(), names, precedence, true); } private String function(FormulaAst.FunctionCall call, Map names) { if (!functions.contains(call.getName())) { throw new IllegalArgumentException("unsupported formula function"); } List arguments = new ArrayList<>(); for (FormulaAst.Node argument : call.getArguments()) { arguments.add(renderNode(argument, names, 0, false)); } String name = call.getName(); if ("ABS".equals(name)) { return "\\left|" + arguments.get(0) + "\\right|"; } if ("SQRT".equals(name)) { return "\\sqrt{" + arguments.get(0) + "}"; } if ("POW".equals(name)) { return powerBase(call.getArguments().get(0), arguments.get(0)) + "^{" + arguments.get(1) + "}"; } if ("NTH_ROOT".equals(name)) { return "\\sqrt[" + arguments.get(1) + "]{" + arguments.get(0) + "}"; } if ("LN".equals(name)) { return "\\ln\\left(" + arguments.get(0) + "\\right)"; } if ("LOG10".equals(name)) { return "\\log_{10}\\left(" + arguments.get(0) + "\\right)"; } if ("LOG".equals(name)) { return "\\log_{" + arguments.get(1) + "}\\left(" + arguments.get(0) + "\\right)"; } if ("EXP".equals(name)) { return "e^{" + arguments.get(0) + "}"; } if ("MIN".equals(name) || "MAX".equals(name)) { return "\\" + name.toLowerCase() + "\\left(" + join(arguments) + "\\right)"; } if ("SUM".equals(name)) { return "\\sum\\left(" + arguments.get(0) + "\\right)"; } if ("MEAN".equals(name)) { return "\\overline{" + arguments.get(0) + "}"; } if ("STDDEV_S".equals(name)) { return "s\\left(" + arguments.get(0) + "\\right)"; } if ("STDDEV_P".equals(name)) { return "\\sigma\\left(" + arguments.get(0) + "\\right)"; } if ("VAR_S".equals(name)) { return "s^{2}\\left(" + arguments.get(0) + "\\right)"; } if ("VAR_P".equals(name)) { return "\\sigma^{2}\\left(" + arguments.get(0) + "\\right)"; } return "\\operatorname{" + escapeFunctionName(name) + "}\\left(" + join(arguments) + "\\right)"; } private static String powerBase(FormulaAst.Node node, String rendered) { return precedence(node) < 9 ? "\\left(" + rendered + "\\right)" : rendered; } private static String escapeFunctionName(String name) { return name.replace("_", "\\_"); } private static String literal(FormulaAst.Literal literal) { switch (literal.getKind()) { case DECIMAL: return ((BigDecimal) literal.getValue()).toPlainString(); case TEXT: return "\\text{\"" + escapeText((String) literal.getValue()) + "\"}"; case BOOLEAN: return "\\mathrm{" + literal.getValue().toString() + "}"; case NULL: return "\\mathrm{null}"; default: throw new IllegalArgumentException("unsupported literal kind"); } } private static String latexOperator(String operator) { if ("+".equals(operator) || "-".equals(operator) || ">".equals(operator) || "<".equals(operator)) { return " " + operator + " "; } if ("*".equals(operator)) { return " \\cdot "; } if ("%".equals(operator)) { return " \\bmod "; } if (">=".equals(operator)) { return " \\ge "; } if ("<=".equals(operator)) { return " \\le "; } if ("!=".equals(operator)) { return " \\ne "; } if ("==".equals(operator)) { return " = "; } if ("&&".equals(operator)) { return " \\land "; } if ("||".equals(operator)) { return " \\lor "; } throw new IllegalArgumentException("unsupported binary operator"); } private static String unaryOperator(String operator) { if ("+".equals(operator) || "-".equals(operator)) { return operator; } if ("!".equals(operator)) { return "\\lnot "; } throw new IllegalArgumentException("unsupported unary operator"); } private static int precedence(FormulaAst.Node node) { if (node instanceof FormulaAst.Binary) { String operator = ((FormulaAst.Binary) node).getOperator(); if ("||".equals(operator)) { return 1; } if ("&&".equals(operator)) { return 2; } if ("==".equals(operator) || "!=".equals(operator)) { return 3; } if (">".equals(operator) || ">=".equals(operator) || "<".equals(operator) || "<=".equals(operator)) { return 4; } if ("+".equals(operator) || "-".equals(operator)) { return 5; } if ("*".equals(operator) || "/".equals(operator) || "%".equals(operator)) { return 6; } if ("^".equals(operator)) { return 8; } throw new IllegalArgumentException("unsupported binary operator"); } if (node instanceof FormulaAst.Unary) { return 7; } return 9; } private static String escapeText(String value) { StringBuilder escaped = new StringBuilder(value.length() + 16); for (int i = 0; i < value.length(); i++) { char current = value.charAt(i); switch (current) { case '\\': escaped.append("\\textbackslash{}"); break; case '{': escaped.append("\\{"); break; case '}': escaped.append("\\}"); break; case '_': case '%': case '#': case '&': case '$': escaped.append('\\').append(current); break; case '<': escaped.append("\\textless{}"); break; case '>': escaped.append("\\textgreater{}"); break; case '^': escaped.append("\\textasciicircum{}"); break; case '~': escaped.append("\\textasciitilde{}"); break; case '\n': case '\r': case '\t': escaped.append(' '); break; default: escaped.append(current); } } return escaped.toString(); } private static String join(List values) { StringBuilder joined = new StringBuilder(); for (int i = 0; i < values.size(); i++) { if (i > 0) { joined.append(", "); } joined.append(values.get(i)); } return joined.toString(); } }