Skip to content

Math Evaluator — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// math-evaluator — C# 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.
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

class MathEvaluator
{
    // COPIED VERBATIM from src/lib/math-evaluator.ts — the code-injection guard.
    static readonly Regex SafeChars = new("^[0-9+\\-*/().,\\s a-zA-Z%^!<>]+$");
    static readonly Regex Forbidden = new("\\b(import|require|eval|function|while|for|process|global|this|window|document|constructor)\\b");
    const int MaxLen = 200;
    static readonly Dictionary<string, Func<double, double>> Fn = new() {
        ["sqrt"] = Math.Sqrt, ["sin"] = Math.Sin, ["cos"] = Math.Cos, ["tan"] = Math.Tan,
        ["log"] = Math.Log, ["abs"] = Math.Abs, ["exp"] = Math.Exp,
    };

    string s;
    int i;
    bool err, isBool;
    MathEvaluator(string s) { this.s = s; }

    // '\0' stands in for end-of-input (C# strings carry no NUL terminator).
    char Peek() { while (i < s.Length && (s[i] == ' ' || s[i] == '\t')) i++; return i < s.Length ? s[i] : '\0'; }
    bool 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 (char.IsDigit(c) || c == '.') {
            int start = i;
            while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '.')) i++;
            if (!double.TryParse(s[start..i], out double v)) { err = true; return 0; }
            return v;
        }
        if (char.IsLetter(c)) {
            string name = "";
            while (i < s.Length && char.IsLetter(s[i])) name += char.ToLower(s[i++]);
            if (Fn.TryGetValue(name, out var fn)) {  // one-arg call, e.g. sqrt(2)
                if (!Eat('(')) { err = true; return 0; }
                double v = Cmp();
                err |= !Eat(')');
                return fn(v);
            }
            if (name == "pi") return Math.PI;
            if (name == "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[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[i - 1] == '+' ? v + Mul() : v - Mul();
        return v;
    }
    double Cmp() {  // lowest precedence; parens recurse back here
        double v = Add();
        while (Eat('<') || Eat('>')) {
            bool lt = s[i - 1] == '<';
            double r = Add();
            v = lt ? v < r : v > r;
            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.
    public static string Evaluate(string expr) {
        string t = expr.Trim();
        if (t.Length == 0 || t.Length > MaxLen || !SafeChars.IsMatch(t) || Forbidden.IsMatch(t))
            return null;
        var p = new MathEvaluator(t);
        double v = p.Cmp();
        p.Peek();
        if (p.err || p.i < t.Length) return null;  // parse error or leftover tokens
        return p.isBool ? (v != 0 ? "true" : "false") : v.ToString("G15");
    }

    static void Main() {
        foreach (string c in new[] { "1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt" })
            Console.WriteLine($"{c,-14} = {Evaluate(c) ?? "(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 →