Search code examples
ruby-on-railsrubyparsingparameterseval

Ruby: Parse, replace, and evaluate a string formula


I'm creating a simple Ruby on Rails survey application for a friend's psychological survey project. So we have surveys, each survey has a bunch of questions, and each question has one of the options participants can choose from. Nothing exciting.

One of the interesting aspects is that each answer option has a score value associated with it. And so for each survey a total score needs to be calculated based on these values.

Now my idea is instead of hard-coding calculations is to allow user add a formula by which the total survey score will be calculated. Example formulas:

"Q1 + Q2 + Q3"
"(Q1 + Q2 + Q3) / 3"
"(10 - Q1) + Q2 + (Q3 * 2)"

So just basic math (with some extra parenthesis for clarity). The idea is to keep the formulas very simple such that anyone with basic math can enter them without resolving to some fancy syntax.

My idea is to take any given formula and replace placeholders such as Q1, Q2, etc with the score values based on what the participant chooses. And then eval() the newly formed string. Something like this:

f = "(Q1 + Q2 + Q3) / 2"  # some crazy formula for this survey
values = {:Q1 => 1, :Q2 => 2, :Q3 => 2}  # values for substitution 
result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym] }   # string to be eval()-ed
eval(result)

So my questions are:

  1. Is there a better way to do this? I'm open to any suggestions.

  2. How to handle formulas where not all placeholders were successfully replaced (e.g. one question wasn't answered)? Ex: {:Q2 => 2} wasn't in values hash? My idea was to rescue eval() but it wouldn't fail in this case coz (1 + + 2) / 2 can still be eval()-ed... any thoughts?

  3. How to get proper result? Should be 2.5, but due to integer arithmetic, it will truncate to 2. I can't expect people who provide the correct formula (e.g. / 2.0 ) to understand this nuance.

  4. I do not expect this, but how to best protect eval() from abuse (e.g. bad formula, manipulated values coming in)? Example: f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 '

Thank you!


Solution

  • OK, now it's totally safe. I swear!

    I would normally clone the formula variable but in this case since you're worried about a hostile user I cleaned the variable in place:

    class Evaluator
    
      def self.formula(formula, values)
        # remove anything but Q's, numbers, ()'s, decimal points, and basic math operators 
        formula.gsub!(/((?![qQ0-9\s\.\-\+\*\/\(\)]).)*/,'').upcase!
        begin
          formula.gsub!(/Q\d+/) { |match|
            ( 
              values[match.to_sym] && 
              values[match.to_sym].class.ancestors.include?(Numeric) ?
              values[match.to_sym].to_s :
              '0'
            )+'.0'
          }
          instance_eval(formula)
        rescue Exception => e
          e.inspect
        end
      end
    
    end
    
    f = '(q1 + (q2 / 3) + q3 + (q4 * 2))'  # some crazy formula for this survey
    values = {:Q2 => 1, :Q4 => 2}  # values for substitution 
    puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
    => formula: (0.0 + (1.0 / 3) + 0.0 + (2.0 * 2)) = 4.333333333333333
    
    f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2'  # some crazy formula for this survey
    values = {:Q1 => 1, :Q3 => 2}  # values for substitution 
    puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
    => formula: (1.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.5
    
    f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2'  # some crazy formula for this survey
    values = {:Q1 => 'delete your hard drive', :Q3 => 2}  # values for substitution 
    puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
    => formula: (0.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.0
    
    f = 'system("ruby -v")'  # some crazy formula for this survey
    values = {:Q1 => 'delete your hard drive', :Q3 => 2}  # values for substitution 
    puts "formula: #{f} = #{Evaluator.formula(f,values)}"  
    => formula: ( -) = #<SyntaxError: (eval):1: syntax error, unexpected ')'>