2014-02-19 4 views
0

TaxArray класс наследует от Array класса:рубин метод цепочки ошибок

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 

Он содержит hashes налогов:

@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'} 

Метод for_type работает, как ожидалось:

> @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'}] 

for_region метод работает, как ожидалось:

> @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"}] 

Однако, когда я цепь методы вместе, я получаю сообщение об ошибке:

> @taxes.for_type(:liquor).for_region('WI') 
NoMethodError: undefined method `for_region' for #<Array:0x007f90320d7c20> 

Каждый метод возвращает Array, а не TaxArray.

Должен ли я отбрасывать возвращаемое значение каждого метода на TaxArray или есть ли другой способ?

+0

вам либо нужно переопределить 'select', или просто явно возвращают новые' TaxArray's из ваших методов. 'select' по умолчанию возвращает новый массив. – roippi

+0

Как бы вы его бросили? В любом случае, если вы хотите, чтобы 'select' возвращал' TaxArray', вам нужно вернуть 'TaxArray', иначе он просто вернет' Array'. –

+1

Обратите внимание, что 'self.select' совпадает с' select', потому что если получатель не указан, 'self' является значением по умолчанию (но' self' * is * необходимо в 'self.class' в ответе @ Zach, для указания ключевого слова 'class' не предусмотрено). –

ответ

1

Не уверен, что это лучшее решение, но я хотел бы сделать это следующим образом:

class TaxArray < Array 

    ... 

    def select 
    self.class.new(super) 
    end 

end 
2

Вообще, я бы не рекомендовал подклассов Рубиновые примитивы, ровно по причинам вы натыкаясь. Это так же просто включить переменную экземпляра массива и работать на том, что:

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 

Вы также можете просто определить весь ваш апи одним махом с помощью 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 

А почему бы не сделать еще один шаг далее и пересылать все неизвестные методы массиву с предположением, что этот класс должен реагировать на что-либо, что было бы в массиве:

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 
+1

Я бы определил 'select' в любом случае, даже в этом случае. Зачем повторять бросок в 'for_region'? –

+0

Мне нравится подход 'missing_method'. Есть ли способ адаптировать его так, чтобы поиск 'name' использовал' regexp'? – craig

+0

@craig: да! Вы можете изменить условный оператор (строка с 'response_to?') На все, что пожелаете. 'name' является символом, но вы можете вызвать' to_s' или использовать интерполяцию строк ("# {name}") и применить соответствие регулярных выражений. –

0

Это потому, что ваши методы r eturn plain array, и у него нет другого метода.

Вы можете сделать ваши методы возвращают объект TaxArray так:

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 
0

Я думаю, что проблема была бы намного проще, если бы вместо того, чтобы TaxArray < Array вы реализовали простой Tax класс как следующий

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 

и выполнил требуемые операции в (обычный) массив из Tax экземпляров.

0

Для этого вы можете использовать Module#refine (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 = [] 

Теперь добавим некоторые данные (я удалил хэш-элемент с ключом :rate немного упростить):

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"}] 

Одним из ограничений в использовании refine является: "You may only activate refinements at top-level...", что предотвращает, очевидно, тестирование в Pry и IRB.

В качестве альтернативы, где-то я читал, что это должно работать :-):

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') 
Смежные вопросы