プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

自作クラスのインスタンスをtallyで集計する

独自に作ったクラスを何らかの基準で「同じもの」と判断させtallyメソッドで集計したい。

ここではDrinkクラスを定義しname(商品名)とprice(値段)がそれぞれ等しいなら「同じ(同値)」だとみなしてドリンクが何本あるか集計したいとする。

やりたいことは以下の通り

Array.new(5) { Drink.new('リンゴジュース', 150) }.tally 
#=> {<<リンゴジュース>>=>5}

普通にクラスを定義してtallyメソッドを読んでも「同じ」とは判断してくれないのでうまくいかない。

class Drink
  attr_reader :name, :price
  
  def initialize(name, price)
    @name = name
    @price = price
  end

  def inspect
    "<<#{name}>>"
  end
end

Array.new(5) { Drink.new('リンゴジュース', 150) }.tally
# => {
# =>  <<リンゴジュース>>=>1,
# =>  <<リンゴジュース>>=>1,
# =>  <<リンゴジュース>>=>1,
# =>  <<リンゴジュース>>=>1,
# =>  <<リンゴジュース>>=>1,
# =>  <<リンゴジュース>>=>1
# => }

ちなみに Array.new(5, Drink(..) }みたいな呼び出しをすると集計はされるがオブジェクトIDが等しくなるので本意ではないとします。

tallyはeql?メソッドやhashメソッドを用いて「同じ」と判定している。 なのでこれらのメソッドを適切にオーバーライドする。今回は商品名(name)が等しい かつ 値段が等しいものは「同じ」ものとして集計したいので以下のようなコードになる。

class Drink
  attr_reader :name, :price

  def initialize(name, price)
    @name = name
    @price = price
  end

  private

  def inspect
    "<<#{name}>>"
  end

  def eql?(other)
    name == other.name && price == other.price
  end

  def hash
    [price, name].hash
  end
end

Array.new(5) { Drink.new('リンゴジュース', 150) }.tally
#=> {<<リンゴジュース>>=>5}

めでたし。めでたし。

参考

備考

仲間とモブプロしているときに思いついてあれこれしてたらうまくいきました。いろいろアドバイスをくれた仲間に感謝します。