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?

有帮助吗?

解决方案 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

其他提示

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')
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top