Skip to content

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

// math-evaluator — Swift port: safe arithmetic expression evaluator (no eval).
// Guards with the TS lib's patterns verbatim (NSRegularExpression / ICU), then
// runs the Go twin's recursive-descent grammar: comparison < > → + - → * / %
// → ^ (right-assoc) → unary - → postfix ! → primary (number, parens, pi/e,
// one-arg calls). Unsafe or invalid input yields nil, never throws.

import Foundation

// COPIED VERBATIM from src/lib/math-evaluator.ts — the code-injection guard.
let safeChars = try! NSRegularExpression(pattern: "^[0-9+\\-*/().,\\s a-zA-Z%^!<>]+$")
let forbidden = try! NSRegularExpression(pattern: "\\b(import|require|eval|function|while|for|process|global|this|window|document|constructor)\\b")
let maxLen = 200
let fns: [String: (Double) -> Double] = [
    "sqrt": sqrt, "sin": sin, "cos": cos, "tan": tan,
    "log": log, "abs": abs, "exp": exp,
]

func matches(_ re: NSRegularExpression, _ s: String) -> Bool {
    re.firstMatch(in: s, range: NSRange(s.startIndex..., in: s)) != nil
}

final class Parser {
    let s: [Character]
    var i = 0
    var err = false, isBool = false
    init(_ s: String) { self.s = Array(s) }

    // nil stands in for end-of-input.
    func peek() -> Character? {
        while i < s.count, s[i] == " " || s[i] == "\t" { i += 1 }
        return i < s.count ? s[i] : nil
    }
    func eat(_ c: Character) -> Bool {
        guard peek() == c else { return false }
        i += 1
        return true
    }
    func factorial(_ n: Double) -> Double {
        if n < 0 || n != n.rounded(.down) { err = true; return 0 }
        var r = 1.0
        if n >= 2 { for k in stride(from: 2.0, through: n, by: 1.0) { r *= k } }
        return r
    }
    func primary() -> Double {
        guard let c = peek() else { err = true; return 0 }
        if c == "(" { i += 1; let v = cmp(); err = err || !eat(")"); return v }
        if c.isNumber || c == "." {
            let start = i
            while i < s.count, s[i].isNumber || s[i] == "." { i += 1 }
            guard let v = Double(String(s[start..<i])) else { err = true; return 0 }
            return v
        }
        if c.isLetter {
            var name = ""
            while i < s.count, s[i].isLetter { name.append(s[i].lowercased()); i += 1 }
            if let f = fns[name] {  // one-arg call, e.g. sqrt(2)
                guard eat("(") else { err = true; return 0 }
                let v = cmp()
                err = err || !eat(")")
                return f(v)
            }
            if name == "pi" { return .pi }
            if name == "e" { return M_E }
        }
        err = true  // bare function reference or unknown symbol → rejected
        return 0
    }
    func post() -> Double { var v = primary(); while eat("!") { v = factorial(v) }; return v }
    func unary() -> Double {
        if eat("-") { return -unary() }
        if eat("+") { return unary() }
        return post()
    }
    func powr() -> Double { let l = unary(); return eat("^") ? pow(l, powr()) : l }
    func mul() -> Double {
        var v = powr()
        while eat("*") || eat("/") || eat("%") {
            let op = s[i - 1]
            let r = powr()
            v = op == "*" ? v * r : op == "/" ? v / r : v.truncatingRemainder(dividingBy: r)
        }
        return v
    }
    func add() -> Double {
        var v = mul()
        while eat("+") || eat("-") { v = s[i - 1] == "+" ? v + mul() : v - mul() }
        return v
    }
    // Lowest precedence; parens recurse back here. Comparisons yield 1/0,
    // remembered in isBool so the caller can print "true"/"false".
    func cmp() -> Double {
        var v = add()
        while eat("<") || eat(">") {
            let lt = s[i - 1] == "<"
            let 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 nil for empty/over-long/unsafe input or any parse failure. Integer-
// valued doubles drop the ".0", matching JS String(number).
func evaluate(_ expr: String) -> String? {
    let t = expr.trimmingCharacters(in: .whitespacesAndNewlines)
    if t.isEmpty || t.count > maxLen || !matches(safeChars, t) || matches(forbidden, t) { return nil }
    let p = Parser(t)
    let v = p.cmp()
    _ = p.peek()  // skip trailing whitespace before the leftover check
    if p.err || p.i < p.s.count { return nil }  // parse error or leftover tokens
    if p.isBool { return v != 0 ? "true" : "false" }
    if v == v.rounded(.down), v.magnitude < 1e15 { return String(Int(v)) }
    return String(v)
}

for c in ["1 + 2 * 3", "2^10", "5! + sqrt(16)", "sin(pi)", "3 > 2", "sqrt"] {
    print("\(c.padding(toLength: 14, withPad: " ", startingAt: 0)) = \(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 →