Skip to content

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

// math-evaluator — Kotlin 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 kotlin.math.E
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.tan

// COPIED VERBATIM from src/lib/math-evaluator.ts — the code-injection guard.
private val SAFE = Regex("^[0-9+\\-*/().,\\s a-zA-Z%^!<>]+$")
private val FORBIDDEN = Regex("\\b(import|require|eval|function|while|for|process|global|this|window|document|constructor)\\b")
private const val MAX_LEN = 200
private val FN: Map<String, (Double) -> Double> = mapOf(
    "sqrt" to ::sqrt, "sin" to ::sin, "cos" to ::cos, "tan" to ::tan,
    "log" to ::ln, "abs" to ::abs, "exp" to ::exp)

private class Parser(val s: String) {
    var i = 0
    var err = false
    var isBool = false

    // '\u0000' stands in for end-of-input.
    fun peek(): Char {
        while (i < s.length && (s[i] == ' ' || s[i] == '\t')) i++
        return if (i < s.length) s[i] else '\u0000'
    }
    fun eat(c: Char): Boolean {
        if (peek() == c && c != '\u0000') { i++; return true }
        return false
    }
    fun factorial(n: Double): Double {
        if (n < 0 || n != floor(n)) { err = true; return 0.0 }
        var r = 1.0
        for (k in 2..n.toInt()) r *= k
        return r
    }
    fun primary(): Double {
        val c = peek()
        if (c == '(') { i++; val v = cmp(); err = err || !eat(')'); return v }
        if (c.isDigit() || c == '.') {
            val start = i
            while (i < s.length && (s[i].isDigit() || s[i] == '.')) i++
            return s.substring(start, i).toDoubleOrNull() ?: run { err = true; 0.0 }
        }
        if (c.isLetter()) {
            val name = buildString { while (i < s.length && s[i].isLetter()) append(s[i++].lowercaseChar()) }
            val fn = FN[name]
            if (fn != null) {  // one-arg call, e.g. sqrt(2)
                if (!eat('(')) { err = true; return 0.0 }
                val v = cmp()
                err = err || !eat(')')
                return fn(v)
            }
            if (name == "pi") return PI
            if (name == "e") return E
        }
        err = true  // bare function reference or unknown symbol → rejected
        return 0.0
    }
    fun post(): Double { var v = primary(); while (eat('!')) v = factorial(v); return v }
    fun unary(): Double = if (eat('-')) -unary() else if (eat('+')) unary() else post()
    fun powr(): Double { val l = unary(); return if (eat('^')) l.pow(powr()) else l }
    fun mul(): Double {
        var v = powr()
        while (eat('*') || eat('/') || eat('%')) {
            val op = s[i - 1]
            val r = powr()
            v = when (op) { '*' -> v * r; '/' -> v / r; else -> v % r }
        }
        return v
    }
    fun add(): Double {
        var v = mul()
        while (eat('+') || eat('-')) v = if (s[i - 1] == '+') v + mul() else v - mul()
        return v
    }
    fun cmp(): Double {  // lowest precedence; parens recurse back here
        var v = add()
        while (eat('<') || eat('>')) {
            val lt = s[i - 1] == '<'
            val r = add()
            v = if (lt) { if (v < r) 1.0 else 0.0 } else { if (v > r) 1.0 else 0.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 String(number).
fun evaluate(expr: String): String? {
    val t = expr.trim()
    if (t.isEmpty() || t.length > MAX_LEN || !SAFE.matches(t) || FORBIDDEN.containsMatchIn(t)) return null
    val p = Parser(t)
    val v = p.cmp()
    p.peek()
    if (p.err || p.i < t.length) return null  // parse error or leftover tokens
    if (p.isBool) return if (v != 0.0) "true" else "false"
    return if (v == floor(v) && !v.isInfinite() && abs(v) < 1e15) v.toLong().toString() else v.toString()
}

fun main() {
    for (c in listOf("1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt")) {
        val r = evaluate(c)
        println("%-14s = %s".format(c, 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 →