rubyrpntest-first

Testfirst.org - rpn_calculator - how can I pass a variable between methods?


I am trying to do this lesson and I am clearly missing something glaringly obvious!

require "rpn_calculator"

describe RPNCalculator do

  attr_accessor :calculator

  before do
    @calculator = RPNCalculator.new
  end

  it "adds two numbers" do
    calculator.push(2)
    calculator.push(3)
    calculator.plus
    calculator.value.should == 5
  end

  ...

  # extra credit
  it "evaluates a string" do
    calculator.evaluate("1 2 3 * +").should ==
      ((2 * 3) + 1)

  ...

  end

end

Everything is working to my novice eyes except @numbers. setting @numbers from the evaluate method doesn't affect @numbers in other methods and I really don't understand why. I've tried everything I could google including changing @numbers to @@numbers but nothing seems to help. I could just evaluate the string in the evaluate method... but I already have such a nice plus method that I can use!

class RPNCalculator
  attr_accessor :numbers

  def initialize
    @numbers = []
  end  

  def push(n)
    @numbers.push(n)  
  end

  def plus
    @numbers.length > 1 ? @numbers.push(@numbers.pop(2).reduce(:+) ) : fail
  end

  def minus
    @numbers.length > 1 ? @numbers.push(@numbers.pop(2).reduce(:-) ) : fail
  end  

  def divide
    @numbers.length > 1 ? @numbers.push(@numbers.pop(2).inject{|x,y| x.to_f / y} ) : fail
  end

  def times
    @numbers.length > 1 ? @numbers.push(@numbers.pop(2).reduce(:*) ) : fail
  end

  def value
    @value = @numbers[-1]
  end

  def tokens(pol)
    pol.split(' ').map{|n| n.to_i.to_s == n ? n.to_i : n.to_sym}
  end

  def evaluate(pol)
    order = []
    opps = {:+ => plus, :- => minus, :/ => divide, :* => times }
    tokens(pol).reverse.chunk{|n| n.is_a?(Integer)}.each{|e,a| e == true ? a.reverse.each{|a| push(a) } : a.each {|a| order.push(a) }}
    order.reverse.each {|o| (opps[o]) }
  end

  def fail
    begin
      raise Exception.new("calculator is empty")
    end
  end

end

The result is the plus returns fail because @numbers is empty....

RPNCalculator
  adds two numbers
  adds three numbers
  subtracts the second number from the first number
  adds and subtracts
  multiplies and divides
  resolves operator precedence unambiguously
  fails informatively when there's not enough values stacked away
  tokenizes a string
  evaluates a string (FAILED - 1)

Failures:

  1) RPNCalculator evaluates a string
     Failure/Error: calculator.evaluate("1 2 3 * +").should ==
     Exception:
       calculator is empty
     # ./12_rpn_calculator/rpn_calculator.rb:59:in `fail'
     # ./12_rpn_calculator/rpn_calculator.rb:14:in `plus'
     # ./12_rpn_calculator/rpn_calculator.rb:39:in `evaluate'
     # ./12_rpn_calculator/rpn_calculator_spec.rb:134:in `block (2 levels) in '

Thanks to Frank Schmitt I got it working. Apparently one does not simply store methods in hashes.

Correct evaluate method:

  def evaluate(pol)
    @numbers = [] # because this does 4 tests without clearing @numbers
    opps = {:+ => Proc.new {plus}, :- => Proc.new{minus}, :/ => Proc.new{divide}, :* => Proc.new{times} } # method in proc, thank you Frank :D
    tokens(pol).chunk{|n| n.is_a?(Integer)}.each{|e,a| e == true ? a.each{|a| push(a) } : a.each {|o| (opps[o].call) }}
    @numbers[0]
  end


Solution

  • Apparently, Ruby calls the methods as soon as you store them inside your opps hash. To fix this, you can use this modified version of your code:

      def evaluate(pol)
        order = []
        opps = {:+ => Proc.new {plus}, :- => Proc.new{minus}, :/ => Proc.new{divide}, :* => Proc.new{times} }
        tokens(pol).reverse.chunk{|n| n.is_a?(Integer)}.each{|e,a| e == true ? a.reverse.each{|a| push(a) } : a.each {|a| or
    der.push(a) }}
        order.reverse.each {|o| (opps[o].call) }
      end
    

    This version