user1440995
user1440995

Reputation: 31

Evaluating a user-input expression in Rails

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

Answers (3)

Shriram Balakrishnan
Shriram Balakrishnan

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

Anil
Anil

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

Lucas Wiman
Lucas Wiman

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

Related Questions