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:
Is there a better way to do this? I'm open to any suggestions.
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?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.
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!
It might not be worth the effort, but if I were to do this I would use Treetop to define a parsing grammar. There are even examples out there for using PEG-style grammars like this for simple arithmetic, so you'd be 90% of the way for the grammar, and most of the way towards evaluating the weighting.
Re 2) Even though that's ugly, you could just create a Hash with default values, and make sure that that fails when
to_s
is called on it (I did say that's ugly, right?):Re 3) Just make sure you have at least one float in your calculation. The easiest way would be to just turn all of the provided values into floats during the replacements:
Re 4) you might want to read up on
$SAFE
. The "Pickaxe" actually contains an example abouteval
ing something entered in a web form:http://ruby-doc.org/docs/ProgrammingRuby/html/taint.html
This is if you really wanna go down the
eval
route, don't ignore the alternatives provided in this discussion.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:Use Dentaku:
You can use RubyParser to interpret the expression e iterate by the nodes to check if exist any dangerous code, like a function call. Look:
This just allow operators, numbers and specifc constants.