Reputation: 31
I need to accept an mathematical expression (including one or more unknowns) from the user and substitute values in for the unknowns to get a result.
I could use eval() to do this, but it's far too risky unless there is a way to recognise "safe" expressions.
I'd rather not write my own parser if I can help it.
I searched for a ready-made parser but the only one I found ( https://www.ruby-toolbox.com/gems/expression_parser , which seems to be the same as the one discussed at http://lukaszwrobel.pl/blog/math-parser-part-4-tests) seems to be limited to the "four rules" +-*/. I need to include exponential, log and trig functions at the very least.
Any suggestions?
UPDATE: EXAMPLE
include Math
def exp(x)
Math.exp(x)
end
def cos(x)
Math.cos(x)
end
pi=Math::PI
t=2
string= '(3*exp(t/2)*cos(3*t-pi/2))'
puts eval(string)
UPDATE - a pre-parsing validation step
I think I will use this regex to check the string has the right kinds of tokens in it:
/((((cos\(|sin\()|(tan\(|acos\())|((asin\(|atan\()|(exp\(|log\())|ln\()(([+-\/*^\(\)A-Z]|\d)+))*([+-\/*^\(\)A-Z]|\d)+/
But I will still implement the parsing method during the actual evaluation.
Thanks for the help!
Upvotes: 3
Views: 2466
Reputation: 509
You can checkout the Dentaku gem - https://github.com/rubysolo/dentaku
You can use it to execute the user given formula.
Here is an example usage of this gem.
class FormulaExecutor
def execute_my_formula(formula, params)
calc = Dentaku::Calculator.new
# Param 1 => formula to execute
# Param 2 => Hash of local variables
calc.evaluate(formula, params)
end
end
FormulaExecutor.new.execute_my_formula( "length * breadth", {'length' => 11, 'breadth' => 120} )
Upvotes: 2
Reputation: 3919
Start with the assumption that eval doesn't exist unless you have a very tight grip on the evaluated content. Even if you don't parse, you could split all input into tokens and check that each is an acceptable token.
Here is a very crude way to check that input has nothing other than valid tokens. Lots of refactoring/ improvments possible.
include Math
def exp(x)
Math.exp(x)
end
def cos(x)
Math.cos(x)
end
pi=Math::PI
t=2
a = %Q(3*exp(t/2)*cos(3*t-pi/2)) # input string
b = a.tr("/*)([0-9]-",'') # remove all special single chars
b.gsub!(/(exp|cos|pi|t)/,'') # remove all good tokens
eval(a) if b == '' # eval if nothing other than good tokens.
Upvotes: 0
Reputation: 11247
If eval would work, then you could parse the expression using a ruby parser (eg gem install ruby_parser
), and then evaluate the S expression recursively, either ignoring or raising an error on non-arithmetic functions. This probably needs some work, but sounded like fun:
require 'ruby_parser'
def evaluate_arithmetic_expression(expr)
parse_tree = RubyParser.new.parse(expr) # Sexp < Array
return evaluate_parse_tree(parse_tree)
end
def evaluate_parse_tree(parse_tree)
case parse_tree[0]
when :lit
return parse_tree[1]
when :call
meth = parse_tree[2]
if [:+, :*, :-, :/, :&, :|, :**].include? meth
val = evaluate_parse_tree parse_tree[1]
arglist = evaluate_parse_tree parse_tree[3]
return val.send(meth, *arglist)
else
throw 'Unsafe'
end
when :arglist
args = parse_tree[1..-1].map {|sexp| evaluate_parse_tree(sexp) }
return args
end
end
You should be able to enhance this to include things like cos
, sin
, etc. pretty easily. It works for some simple examples I tried, and and includes a free check for well-formedness (parsing raises a Racc::ParseError
exception if not).
Upvotes: 0