Not sure it's the best solution but I would do it this way:
class TaxArray < Array
...
def select
self.class.new(super)
end
end
Pregunta
The TaxArray
class inherits from the Array
class:
class TaxArray < Array
# instance methods
def for_region(region_code)
self.select{|tax|tax[:region_code].include?(region_code)}
end
def for_type(type)
self.select{|tax|tax[:type].include?(type)}
end
end
It contains hashes
of taxes:
@taxes=TaxArray.new
@taxes << {name: "Minnesota Sales", rate: 6.875/100, type: [:food,:liquor], region_code: 'MN'}
@taxes << {name: "Downtown Liquor", rate: 3.0/100, type: [:liquor], region_code: 'MN'}
@taxes << {name: "Downtown Restaurant", rate: 3.0/100, type: [:food], region_code: 'MN'}
# fictitious WI rates
@taxes << {name: "Wisconsin Sales", rate: 5.0/100, type: [:food,:liquor], region_code: 'WI'}
@taxes << {name: "Wisconsin Food", rate: 2.0/100, type: [:food], region_code: 'WI'}
@taxes << {name: "Wisconsin Liquor", rate: 1.0/100, type: [:liquor], region_code: 'WI'}
The for_type
method works as expected:
> @taxes.for_type(:liquor)
=> [{name: "Minnesota Sales", rate: 6.875/100, type: [:food,:liquor], region_code: 'MN'},{name: "Downtown Liquor", rate: 3.0/100, type: [:liquor], region_code: 'MN'},{name: "Wisconsin Sales", rate: 5.0/100, type: [:food,:liquor], region_code: 'WI'},{name: "Wisconsin Liquor", rate: 1.0/100, type: [:liquor], region_code: 'WI'}]
The for_region
method works as expected:
> @taxes.for_region('WI')
=> [{:name=>"Wisconsin Sales", :rate=>0.06, :type=>[:food, :liquor], :region_code=>"WI"}, {:name=>"Wisconsin Food", :rate=>0.02, :type=>[:food], :region_code=>"WI"}, {:name=>"Wisconsin Liquor", :rate=>0.01, :type=>[:liquor], :region_code=>"WI"}]
However, when I chain the methods together, I get an error:
> @taxes.for_type(:liquor).for_region('WI')
NoMethodError: undefined method `for_region' for #<Array:0x007f90320d7c20>
Each method returns an Array
, rather than a TaxArray
.
Should I cast the returned value of each method to a TaxArray
or is there another way?
Solución 2
Not sure it's the best solution but I would do it this way:
class TaxArray < Array
...
def select
self.class.new(super)
end
end
Otros consejos
Generally, I wouldn't recommend subclassing Ruby primitives, for exactly the reasons you're bumping into. It's just as simple to include an array instance variable and operate on that:
class TaxArray
attr_reader :tax_hashes
def initialize(tax_hashes)
@tax_hashes = tax_hashes
end
def for_type(type)
self.class.new(tax_hashes.select {|h| h[:type] == type })
end
end
You could also just define your whole api in one fell swoop using define_method
:
class TaxArray
attr_reader :tax_hashes
def initialize(hashes)
@tax_hashes = hashes
end
[:name, :rate, :type, :region_code].each do |attr|
define_method :"for_#{attr}" do |arg|
self.class.new tax_hashes.select {|tax| Array(tax[attr]).include?(arg) }
end
end
end
And why not go one step further, and forward all unknown methods to the array, with the assumption that this class should respond to anything the array would:
class TaxArray
def method_missing(name, *args, &block)
if tax_hashes.respond_to?(name)
self.class.new(tax_hashes.send(name, *args, &block))
else
super
end
end
end
It is because your methods return plain array objects and it does not have the other method.
You can make your methods return a TaxArray object like so:
class TaxArray < Array
def for_region(region_code)
array = self.select{|tax|tax[:region_code].include?(region_code)}
TaxArray.new(array)
end
def for_type(type)
array = self.select{|tax|tax[:type].include?(type)}
TaxArray.new(array)
end
end
I think the problem would be much simpler if instead of TaxArray < Array
you implemented a simple Tax
class like the following
class Tax
attr_reader :name, :rate, :type, :region_code
def initialize(name, rate, type, region_code)
@name = name
@rate = rate
@type = type
@region_code = region_code
end
def for_region(r_code)
region_code.include?(r_code)
end
def for_type(t)
type.include?(t)
end
end
and performed the desired operations to a (usual) array of Tax
instances.
You could use Module#refine for this (v2.1):
module M
refine Array do
def for_region(region_code)
select { |tax|tax[:region_code].include?(region_code) }
end
def for_type(type)
select { |tax|tax[:type].include?(type) }
end
end
end
using M
taxes = []
Now add some data (I've removed the hash element with key :rate
to simplify slightly):
taxes << {name: "Minnesota Sales", type: [:food,:liquor], region_code: 'MN'}
taxes << {name: "Downtown Liquor", type: [:liquor], region_code: 'MN'}
taxes << {name: "Downtown Restaurant",type: [:food], region_code: 'MN'}
# fictitious WI rates
taxes << {name: "Wisconsin Sales", type: [:food,:liquor], region_code: 'WI'}
taxes << {name: "Wisconsin Food", type: [:food], region_code: 'WI'}
taxes << {name: "Wisconsin Liquor", type: [:liquor], region_code: 'WI'}
p taxes.for_type(:liquor)
[{:name=>"Minnesota Sales", :type=>[:food, :liquor], :region_code=>"MN"},
{:name=>"Downtown Liquor", :type=>[:liquor], :region_code=>"MN"},
{:name=>"Wisconsin Sales", :type=>[:food, :liquor], :region_code=>"WI"},
{:name=>"Wisconsin Liquor", :type=>[:liquor], :region_code=>"WI"}]
p taxes.for_region('WI')
[{:name=>"Wisconsin Sales", :type=>[:food, :liquor], :region_code=>"WI"},
{:name=>"Wisconsin Food", :type=>[:food], :region_code=>"WI"},
{:name=>"Wisconsin Liquor", :type=>[:liquor], :region_code=>"WI"}]
p taxes.for_type(:liquor).for_region('WI')
[{:name=>"Wisconsin Sales", :type=>[:food, :liquor], :region_code=>"WI"},
{:name=>"Wisconsin Liquor", :type=>[:liquor], :region_code=>"WI"}]
One of the restrictions in the use of refine
is: "You may only activate refinements at top-level...", which prevents evidently testing in Pry and IRB.
Alternatively, somewhere I read that this should work :-):
def for_region(taxes, region_code)
taxes.select{|tax|tax[:region_code].include?(region_code)}
end
def for_type(taxes, type)
taxes.select{|tax|tax[:type].include?(type)}
end
for_region(for_type(taxes, :liquor), 'WI')