Find a mistery number in a equation in Ruby

Viewed 153

So i am learning Ruby and got stuck in one exercise. I need find a mistery number in a equation that is in a string.For example ('10 + ? = 18'). The course i am following ask to use eval. I know eval is dangerous, is just for learning purpose. I am using rspec to test, so dont need use if and return

All i got is this

class MisteryNumber
  def calculate(operation)
    count = 0
    expression = operation.split(' = ')
    expression[0].gsub!("?", "#{count}")
    (eval(expression[0] - eval(expression[1])
  end
end

this code only works if i have subtraction or addition equations. If the equation is like this ('100 / 5 * ? = 40'), doesnt work anymore. I tried to do some form of iterator using gsub(thats why i have the 'count' in the code) but no luck. How i could od this?

2 Answers

I assume that '?' appears at the end of the left side of the equality and that the equation has a solution (the equation is not, for example, ('0 * ? = 1'). Both of these assumptions could be relaxed at the expense of somewhat more complex code. I will leave it to the reader to make those adjustments if desired.

We can write the main method as follows.

def calc(str)
  str = str.gsub(/[ =]/, '') 
  (str = str.insert(0,'+')) if str.match?(/\A\d/)
  left_str, s, right_str = str.partition(/[\+\-\/\*]\?/)
  qop = s[0]
  left_str = reduce_left(left_str)
  coeff, right_val = shift_to_right(left_str, right_str.to_f)
  solve(coeff, right_val, qop)
end

For example,

calc "3 * 7 / 3 + 4 / 2 * ? = 12"
  #=> 2.5
calc "2 - 4 / 2 + 8 / 2 + ? = 20"
  #=> 16.0

Suppose

str = "3 * 7 / 3 + 4 / 2 * ? = 12"

The first two steps are to adjust the formatting to simplify the following operations.

str = str.gsub(/[ =]/, '') 
  #=> "3*7/3+4/2*?12"

(str = str.insert(0,'+')) if str.match?(/\A\d/)
  #=> "+3*7/3+4/2*?12"

Now we break the string into three pieces.

left_str, s, right_str = str.partition(/[\+\-\/\*]\?/)
  #=> ["+3*7/3+4/2", "*?", "12"]

so

left_str
  #=> "+3*7/3+4/2"
s #=> "*?"
right_str
  #=> "12"

and save for later:

qop = s[0]
  #=> "*"    

The three regular expressions read as follows.

  • /[ =]/: match a space or equals sign
  • /\A\d/: match a digit at the beginning of the string
  • /[\+\-\/\*]\?/: match a '+', '-', '/' or '*'

The next step is to remove '*' and '/' from left_str. We can do this iteratively until none remain. The following method removes one. This uses the following regular expression:

R = /(\d+(?:\.\d+)?)([\*\/])(\d+(?:\.\d+)?)/

We can write this regular expression in free-spacing mode to make it self-documenting.

R = /
    (            # begin capture group 1
      \d+        # match 1+ digits
      (?:\.\d+)  # match period followed by 1+ digits in a non-capture group
      ?          # make the non-capture group optional
    )            # end capture group 1
    (            # begin the capture group 2
      [\*\/]     # match one character in the character class  
    )            # end capture group 2
    (            # begin capture group 3
      \d+        # match 1+ digits
      (?:\.\d+)  # match period followed by 1+ digits in a non-capture group
      ?          # make the non-capture group optional
    )            # end capture group 3
  /x             # invoke free-spacing regex definition mode
def reduce_left_once(left_str)
  left_str.gsub(R) { $1.to_f.public_send($2, $3.to_f) }
end

Note that m is a MatchData object.

We can test (where left_str #=> "+3*7/3+4/2"):

left_str = reduce_left_once(left_str)
  #=> "+21.0/3+2.0"
left_str = reduce_left_once(left_str)
  #=> "+7.0+2.0"
left_str = reduce_left_once(left_str)
  #=> "+7.0+2.0"

As no change was made in the last step we are finished with this operation. This can be operationalized with the following method.

def reduce_left(left_str)
  loop do
    new_left_str = reduce_left_once(left_str)
    break left_str if left_str == new_left_str
    left_str = new_left_str
  end
end
left_str = "+3*7/3+4/2"
left_str = reduce_left(left_str)
  #=> "+7.0+2.0"

The next step is to shift all terms but the last in left_str to the right of the equality and adjust the right side accordingly. If qop equals '*' or '/' the last term of left_str is '?''s coefficient; else it will be shifted to the right in the last step.

def shift_to_right(left_str, right_val)
  *terms, coeff = left_str.scan(/[\+\-]\d+/)
  terms.each { |s| right_val -= s.to_f }
  [coeff.to_f, right_val]
end
left_str = "+7.0+2.0"
right_str = "12"
coeff, right_val = shift_to_right(left_str, right_str.to_f)
  #=> [2.0, 5.0]

So

coeff
  #=> 2.0
right_val
  #=> 5.0

At this point we have reduced the original expression to

2.0 * x = 5.0

and need to solve for x.

The regular expression /[\+\-]\d+/ in shift_to_right matches '+' or '-' followed by one or more digits.


The last step is to solve this simple linear equation. Recall that we have computed right_val #=> "12" and '?''s operator is given by qop #=> "*".

def solve(coeff, right_val, qop)
  case qop
  when '+' then right_val - coeff
  when '-' then coeff - right_val
  when '*' then right_val/coeff
  else          right_val * coeff
  end
end
solve(coeff, right_val, qop)
  #=> 2.5

Note that most of these operations would be needed if eval were used.

Firstly: what sort of course is it?! This is definitely not a task for beginners!

One option to solve this without a parser is using approximations. It will not work for all the possible equation, but will 100% work when the difference of left and right sides is a monotonic, continuous function and has a good chance working for a good part of not-monotonic but continuous functions as well.

left, right = operation.split('=')
  .map {|side| side.gsub('?', 'x')}
  .map {|str| ->(x) { eval str }}

diff = ->(x) { left.(x) - right.(x) }

The above code will transform given equation into two procs representing left and right side of the equation. We are now looking for such x that diff.(x) == 0.

Next we need to find any two numbers for which diff.(a) * diff.(b) < 0, so that diff.(a) and diff.(b) have different signs. I'd probably start from some reasonable start test numbers (might depend on the common examples given), like [0, 1]. If diff.(0) and diff.(1) have different sign, we move to next step, if not we move the boundaries (preferably exponentially) until we find such numbers. NOTE: as the equation might have no solutions,diff >= 0. However, the proposed approach will work for any monotonic, continuous functions.

Step 2. Having [a,b] such that diff.(a) and diff.(b) we basically start a binary search. We calculate diff.((a+b)/2) and, depending on its sign, we take either a or b so that new pair have opposite signs. We repeat the process until diff.((a+b)/2) is zero. If that was just maths, we would need to establish some precision, but here we already have precision of float, so we can assume that it will eventually happen.

So, this would be the process, I'll leave coding it to you. :) Good luck. And seriously, what course is it?

Related