Q1: attr_accessor_with_history
attr_accessor uses metaprogramming to create getters and setters for object attributes on the fly. Define a method attr_accessor_with_history that provides the same functionality as attr_accessor but also tracks every value the attribute has ever had:
class Foo
attr_accessor_with_history :bar
end
f = Foo.new # => #<Foo:0x127e678>
f.bar = 3 # => 3
f.bar = :wowzo # => :wowzo
f.bar = 'boo!' # => 'boo!'
f.bar_history # => [nil, 3, :wowzo, 'boo!']
Here are some hints and things to notice to get you started:
- The first thing to notice is that if we define attr_accessor_with_history in class Class, we can use it as in the snippet above. This is because in Ruby a class is simply an object of class Class.
- The second thing to notice is that Ruby provides a method class_eval that takes a string and evaluates it in the context of the current class, that is, the class from which you’re calling attr_accessor_with_history. This string will need to contain a method definition that implements a setter-with-history for the desired attribute attr_name.
- bar_history should always return an Array of elements, even if no values have been assigned yet.
- Don’t forget that the very first time the attribute receives a value, its history array will have to be initialized.
- Don’t forget that instance variables are referred to as @bar within getters and setters.
- Although the existing attr_accessor can handle multiple arguments (e.g. attr_accessor :foo, :bar), your version just needs to handle a single argument. However, it should be able to track multiple instance variables per class, with any legal class names or variable names, so it should work if used this way:
- History of instance variables should be maintained separately for each object instance. that is, if you do:
class SomeOtherClass attr_accessor_with_history :foo
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
f = Foo.new
f. bar = 4
f.bar_history
then the last line should just return [nil,4], rather than [nil,1,2,4]. Here is some skeleton code:
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s # make sure it's a string
attr_reader attr_name # create the attribute's getter
attr_reader attr_name+"_history" # create bar_history getter
class_eval "your code here, use %Q for multiline strings"
end
end
class Foo
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
f.bar_history # => if your code works, should be [nil,1,2]
My Example Code
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_reader attr_name + "_history"
#our setter code here
class_eval %Q{
def #{attr_name}=(attr_name)
@#{attr_name} = attr_name
unless @#{attr_name + "_history"}
@#{attr_name + "_history"} = []
@#{attr_name + "_history"} << nil
end
@#{attr_name + "_history"} << attr_name
end
}
end
end
class Test
attr_accessor_with_history :sample
end
t = Test.new
t.sample = "test"
t.sample = 1
t.sample = :abc
print t.sample_history
Q2: Extend Currency Conversion Example
Extend the currency-conversion example from the text so that you can write:
5.dollars.in(:euros)
,10.euros.in(:rupees)
You should support the currencies ‘dollars’, ‘euros’, ‘rupees’, and ‘yen’ where the conversions are: 1 rupee to 0.019 dollars, 1 yen to 0.013 dollars, 1 euro to 1.292 dollars.
Both the singular and plural forms of each currency should be acceptable, e.g.
1.dollar.in(:rupees)
and10.rupees.in(:euro)
should work. You can use the code below (http://pastebin.com/agjb5qBF) as a starting point.
class Numeric
@@currencies = {'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
def method_missing(method_id)
singular_currency = method_id.to_s.gsub( /s$/, '')
if @@currencies.has_key?(singular_currency)
self * @@currencies[singular_currency]
else
super
end
end
end
My Example Code
class Numeric
@@currencies = {'dollar' => 1.000,'yen' => 0.013, 'euro' => 1.292, 'rupee' => 0.019}
def method_missing(method_id)
sigular_currency = method_id.to_s.gsub(/s$/,'')
if @@currencies.has_key?(sigular_currency)
self * @@currencies[sigular_currency]
else
super
end
end
def in(currency)
sigular_currency = currency.to_s.gsub(/s$/,'')
self / @@currencies[sigular_currency]
end
end
puts 5.dollars.in(:euros)
puts 10.euros.in(:rupees)