Math Evaluator — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# math-evaluator — Ruby port: safe arithmetic expression evaluator (no eval).
# Ruby HAS eval — running it on shared-link input is exactly the attack the
# TS lib's guard prevents, so this ports the Go twin's recursive-descent
# parser instead. Guard first (allowlist + keyword blacklist + MAX_LEN), then
# parse; anything unsafe or invalid yields nil.
# COPIED VERBATIM from src/lib/math-evaluator.ts — the code-injection guard.
# (\A..\z instead of ^..$: Ruby's anchors match line boundaries.)
SAFE_CHARS = /\A[0-9+\-*\/().,\s a-zA-Z%^!<>]+\z/
FORBIDDEN = /\b(import|require|eval|function|while|for|process|global|this|window|document|constructor)\b/
MAX_LEN = 200
# Recognized single-arg functions; log is natural log / ln (as in mathjs).
FN = {
'sqrt' => ->(x) { Math.sqrt(x) }, 'sin' => ->(x) { Math.sin(x) },
'cos' => ->(x) { Math.cos(x) }, 'tan' => ->(x) { Math.tan(x) },
'log' => ->(x) { Math.log(x) }, 'abs' => ->(x) { x.abs },
'exp' => ->(x) { Math.exp(x) }
}.freeze
# Recursive-descent parser, precedence low → high: comparison < > → + - →
# * / % → ^ (right-assoc) → unary - → postfix ! → primary (number, parens,
# pi/e, one-arg calls). nil from @s[@i] is the end-of-input sentinel.
class Parser
attr_reader :is_bool
def initialize(src)
@s = src
@i = 0
@err = false
@is_bool = false
end
def peek
@i += 1 while @s[@i] == ' ' || @s[@i] == "\t"
@s[@i]
end
def eat(ch)
return false unless peek == ch
@i += 1
true
end
def error!
@err = true
0.0
end
def factorial(n)
return error! if n.negative? || n != n.truncate
r = 1.0
(2..n.to_i).each { |k| r *= k }
r
end
def primary
c = peek
if c == '('
@i += 1
v = cmp
@err = true unless eat(')')
return v
end
if c =~ /[0-9.]/
text = @s[@i..].match(/\A[0-9.]+/)[0]
@i += text.length
# reject malformed numbers ("." / "1.2.3") that to_f would gloss over
return error! unless text =~ /[0-9]/ && text.count('.') <= 1
text.to_f
elsif c =~ /[a-zA-Z]/
name = +''
while @s[@i] =~ /[a-zA-Z]/
name << @s[@i].downcase
@i += 1
end
if (fn = FN[name])
return error! unless eat('(')
v = cmp
return error! unless eat(')')
fn.call(v)
elsif name == 'pi'
Math::PI
elsif name == 'e'
Math::E
else
error! # bare function reference or unknown symbol → rejected
end
else
error!
end
end
def post
v = primary
v = factorial(v) while eat('!')
v
end
def unary
return -unary if eat('-')
return unary if eat('+')
post
end
def powr
l = unary
eat('^') ? l**powr : l
end
def mul
v = powr
while eat('*') || eat('/') || eat('%')
op = @s[@i - 1]
r = powr
v = op == '*' ? v * r : op == '/' ? v / r : v % r
end
v
end
def add
v = mul
while eat('+') || eat('-')
v = @s[@i - 1] == '+' ? v + mul : v - mul
end
v
end
# Lowest precedence; parens recurse back here. Comparisons yield 1/0,
# remembered in @is_bool so the caller can print "true"/"false".
def cmp
v = add
while eat('<') || eat('>')
lt = @s[@i - 1] == '<'
r = add
v = lt ? (v < r ? 1.0 : 0.0) : (v > r ? 1.0 : 0.0)
@is_bool = true
end
v
end
def parsed_fully?
peek
!@err && @i >= @s.length
end
end
# Mirrors evaluateExpression in src/lib/math-evaluator.ts: formatted result,
# or nil for empty/over-long/unsafe input or any parse failure. Integer-
# valued floats drop the ".0", matching JS String(number).
def evaluate(expr)
t = expr.strip
return nil if t.empty? || t.length > MAX_LEN || t !~ SAFE_CHARS || t =~ FORBIDDEN
parser = Parser.new(t)
v = parser.cmp
return nil unless parser.parsed_fully?
return v != 0 ? 'true' : 'false' if parser.is_bool
v == v.truncate && v.abs < 1e15 ? v.to_i.to_s : v.to_s
end
if $PROGRAM_NAME == __FILE__
['1 + 2 * 3', '2^10', '5! + sqrt(16)', 'sin(pi)', '3 > 2', 'sqrt'].each do |c|
puts format('%-14s = %s', c, evaluate(c) || '(rejected)')
end
end
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 →