Fluid Interface Calculator

Hey all. So I managed to solve this fluid calculator problem but I’m not especially happy with the solution. I saw other solutions using method_missing (e.g. the Calc2 class I include below) but I’m uneasy about method_missing. Any suggestions for improvements here? Thanks in advance…


class Calc
  attr_accessor :str
  def initialize
    self.str = ''
    self
  end

  def plus
    self.str << '+'
    self
  end

  def minus
    self.str << '-'
    self
  end

  def times
    self.str << '*'
    self
  end

  def divided_by
    self.str << '/'
    self
  end

  %w(zero one two three four five six seven eight nine).each_with_index do |num, index|
    define_method "#{num}" do
      if self.str == ''
        self.str << index.to_s
        self
      else
        eval(self.str << index.to_s)
      end
    end
  end
end

# nice n compact, but method_missing blech
class Calc2

  def method_missing(method_name, *args)
    @numbers ||= []
    @numbers << { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, plus: "+", minus: "-", times: "*", divided_by: "/" }[method_name]
    @numbers.size == 3 ? eval(@numbers.join) : self
  end
end
  

require 'minitest/autorun'

class TestCalc < MiniTest::Unit::TestCase

  def setup
    @calc1 = Calc.new.four.plus.five
    @calc2 = Calc.new.five.plus.four
    @calc3 = Calc.new.six.divided_by.two
  end

  def test_1
    assert_equal(9, @calc1)
  end
  
  def test_2
    assert_equal(9, @calc2)
  end

  def test_3
    assert_equal(3, @calc3)
  end
end
 

Why do you use self.str and not @str? Or add a private attr_reader and use str. Also, str is not a very good name, I believe.

I know this is not exactly the help you were looking for, but it’s a start.

Thanks for the feedback, I appreciate it. What do you suggest would be a good name for the variable here? I could’t think of anything particularly descriptive.

I 100% agree that the method_missing version is not the way. I prefer the define_method version if for anything, because you don’t have to remember to add in a respond_to?(method).

Please accept this slight revision, bringing the code at least to a more idiomatic ruby form, and while I am at it, I’ll name your state variable something. It IS a string, but what does it represent? A running ticker for a calculation, thus it’s new name: :calculation_ticker

class Calc
  PRIMARY_INTEGERS = %w(zero one two three four five six seven eight nine)

  attr_reader :calculation_ticker

  def initialize
    @calculation_ticker = String.new
    # you cannot override the return of initialize
  end

  def plus
    @calculation_ticker << '+'
    self
  end

  def minus
    @calculation_ticker << '-'
    self
  end

  def times
    @calculation_ticker << '*'
    self
  end

  def divided_by
    @calculation_ticker << '/'
    self
  end

  PRIMARY_INTEGERS.each_with_index do |num, index|
    define_method "#{num}" do
      if @calculation_ticker == ''
        @calculation_ticker << index.to_s
        self
      else
        eval(@calculation_ticker << index.to_s)
      end
    end
  end
end

require 'minitest/autorun'

class TestCalc < MiniTest::Unit::TestCase

  def setup
    @calc1 = Calc.new.four.plus.five
    @calc2 = Calc.new.five.plus.four
    @calc3 = Calc.new.six.divided_by.two
  end

  def test_1
    assert_equal(9, @calc1)
  end

  def test_2
    assert_equal(9, @calc2)
  end

  def test_3
    assert_equal(3, @calc3)
  end
end

Now that the code is cleaned up a bit I would like to address what I see as a fundamental problem here. Why are you chaining these number string as methods in the first place?

If your goal is to play, great, but this is the kind of dsl code I despise. What about this interface?

calc = Calculator.new
calc.calculate("1 + 1 - 3 +")  #=> -1.0
calc.calculate("5 + 10")       #=> 14.0
calc.clear                     #=> 0.0
calc.calculate("5 + 10")       #=> 15.0
calc.calculate("2")            #=> 17.0 (assumes + when not given)

If was was coding something that was more than a play toy to learn I would code to an interface more like that. What do you think?

What about something like operation or statement?

@zamith, that is better, thanks. @Dreamr, this was just a problem on Code Wars. I hadn’t heard of fluid interfaces, just thought it might be of general interest here.