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 →