Skip to content

Math Evaluator — Java source

Evaluate math expressions - arithmetic, functions (sqrt, sin, log), comparisons, and constants (pi, e) - safely, in real time. Input is constrained to math-safe characters, fully client-side.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// math-evaluator — Java port: safe arithmetic expression evaluator (no eval).
// Guards with the TS lib's patterns verbatim, then runs the Go twin's
// recursive-descent grammar: comparison < > → + - → * / % → ^ (right-assoc)
// → unary - → postfix ! → primary (number, parens, pi/e, 1-arg calls).
// Unsafe or invalid input yields null; never throws.
import java.util.Map;
import java.util.function.DoubleUnaryOperator;
import java.util.regex.Pattern;

class MathEvaluator {
    // COPIED VERBATIM from src/lib/math-evaluator.ts — the code-injection guard.
    static final Pattern SAFE = Pattern.compile("^[0-9+\\-*/().,\\s a-zA-Z%^!<>]+$");
    static final Pattern FORBIDDEN = Pattern.compile("\\b(import|require|eval|function|while|for|process|global|this|window|document|constructor)\\b");
    static final int MAX_LEN = 200;
    static final Map<String, DoubleUnaryOperator> FN = Map.of(
        "sqrt", Math::sqrt, "sin", Math::sin, "cos", Math::cos, "tan", Math::tan,
        "log", Math::log, "abs", Math::abs, "exp", Math::exp);

    static final class Parser {
        final String s;
        int i;
        boolean err, isBool;
        Parser(String s) { this.s = s; }

        // '\0' stands in for end-of-input.
        char peek() { while (i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t')) i++; return i < s.length() ? s.charAt(i) : '\0'; }
        boolean eat(char c) { if (peek() == c && c != '\0') { i++; return true; } return false; }
        double factorial(double n) {
            if (n < 0 || n != Math.floor(n)) { err = true; return 0; }
            double r = 1;
            for (int k = 2; k <= n; k++) r *= k;
            return r;
        }
        double primary() {
            char c = peek();
            if (c == '(') { i++; double v = cmp(); err |= !eat(')'); return v; }
            if (Character.isDigit(c) || c == '.') {
                int start = i;
                while (i < s.length() && (Character.isDigit(s.charAt(i)) || s.charAt(i) == '.')) i++;
                try { return Double.parseDouble(s.substring(start, i)); }
                catch (NumberFormatException e) { err = true; return 0; }
            }
            if (Character.isLetter(c)) {
                String name = "";
                while (i < s.length() && Character.isLetter(s.charAt(i)))
                    name += Character.toLowerCase(s.charAt(i++));
                DoubleUnaryOperator fn = FN.get(name);
                if (fn != null) {  // one-arg call, e.g. sqrt(2)
                    if (!eat('(')) { err = true; return 0; }
                    double v = cmp();
                    err |= !eat(')');
                    return fn.applyAsDouble(v);
                }
                if (name.equals("pi")) return Math.PI;
                if (name.equals("e")) return Math.E;
            }
            err = true;  // bare function reference or unknown symbol → rejected
            return 0;
        }
        double post() { double v = primary(); while (eat('!')) v = factorial(v); return v; }
        double unary() { if (eat('-')) return -unary(); if (eat('+')) return unary(); return post(); }
        double powr() { double l = unary(); if (eat('^')) return Math.pow(l, powr()); return l; }
        double mul() {
            double v = powr();
            while (eat('*') || eat('/') || eat('%')) {
                char op = s.charAt(i - 1);
                double r = powr();
                v = op == '*' ? v * r : op == '/' ? v / r : v % r;
            }
            return v;
        }
        double add() {
            double v = mul();
            while (eat('+') || eat('-')) v = s.charAt(i - 1) == '+' ? v + mul() : v - mul();
            return v;
        }
        double cmp() {  // lowest precedence; parens recurse back here
            double v = add();
            while (eat('<') || eat('>')) {
                boolean lt = s.charAt(i - 1) == '<';
                double r = add();
                v = lt ? (v < r ? 1 : 0) : (v > r ? 1 : 0);
                isBool = true;
            }
            return v;
        }
    }

    // Mirrors evaluateExpression in src/lib/math-evaluator.ts: formatted
    // result, or null for empty/over-long/unsafe input or any parse failure.
    // Integer-valued doubles print without a trailing ".0", matching JS.
    static String evaluate(String expr) {
        String t = expr.trim();
        if (t.isEmpty() || t.length() > MAX_LEN || !SAFE.matcher(t).matches() || FORBIDDEN.matcher(t).find())
            return null;
        Parser p = new Parser(t);
        double v = p.cmp();
        p.peek();
        if (p.err || p.i < t.length()) return null;  // parse error or leftover tokens
        if (p.isBool) return v != 0 ? "true" : "false";
        return v == Math.floor(v) && !Double.isInfinite(v) && Math.abs(v) < 1e15
                ? String.valueOf((long) v) : String.valueOf(v);
    }

    public static void main(String[] args) {
        for (String c : new String[] { "1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt" }) {
            String r = evaluate(c);
            System.out.printf("%-14s = %s%n", c, r != null ? r : "(rejected)");
        }
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →