Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby 3.1: Enumerable#tallyにカウント集計用のオプションハッシュを渡せる(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

tally: (他動詞)〜を勘定する、計算する、数え上げる、集計する、(名詞)勘定
英辞郎オフライン版より

Ruby 3.1: Enumerable#tallyにカウント集計用のオプションハッシュを渡せる(翻訳)

enumeration(列挙)とは、オブジェクト内にあるものを順にたどることを指します。RubyにあるArrayHashRangeなどのクラスは、Enumerableモジュールをインクルードすることで列挙の機能を獲得しています。

このEnumerableモジュールには、#include?#count#map#select#uniqなど、私たちがよく使うさまざまなメソッドが提供されています。本記事で取り上げる#tallyメソッドもそのひとつです。#tallyは、コレクション内に存在する要素の出現回数をカウントして、すべての要素の出現回数を含むハッシュを返します。

Ruby 2.7で導入されたEnumerable#tallyについてもう少し知りたい方は、過去記事(英語)をご覧ください。

サンプルコード

文房具屋を例にとって考えてみましょう。店員は、店の商品を1個単位で販売します。

#tallyメソッドは、1週間の間に販売された商品のトータルの個数を集計するような問題に最適の候補となります。

店の顧客が、以下のように商品を個別に追加して注文をかけるとします。

order_one = %i{ pen pencil eraser sharpener pen pen }
order_two = %i{ sharpener eraser eraser eraser eraser sharpener sharpener sharpener sharpener }

変更前

各商品の数量を以下のように集計します。

tally_one = order_one.tally
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally_two = order_two.tally
# => { sharpener: 5, eraser: 4 }
weekly_tally = (order_one + order_two).tally # order_one, order_two, ..., order_seven
# => { pen: 3, pencil: 1, eraser: 5, sharpener: 6 }

上の例でわかるように、消しゴムと鉛筆削りの個数が増えています。

ここではいったん配列order_oneと配列order_twoを結合し、それから改めて#tallyを呼んで結合後の注文個数の総計を求めています。

変更後

実行中に総計を算出するために、Ruby 3.1のEnumerable#tallyに出現回数集計用のオプションハッシュを渡せるようになりました(#4318)。

この場合、集計途中の項目数を保存する変数を用意して、それを#tallyメソッドに渡せるようになります。

order_one = %i{ pen pencil eraser sharpener pen pen }
order_two = %i{ sharpener eraser eraser eraser eraser sharpener sharpener sharpener sharpener }

weekly_tally = {}

weekly_tally = order_one.tally(weekly_tally)
# => {pen: 3, pencil: 1, eraser: 1, sharpener: 1}

weekly_tally = order_two.tally(weekly_tally)
# => {pen: 3, pencil: 1, eraser: 5, sharpener: 6}

# (以下同様)

上のRuby 3.1コード例でわかるように、項目の出現回数を保存するweekly_tallyハッシュを引数として#tallyメソッドに渡しています。

order_twoにある消しゴムと鉛筆削りの個数がweekly_tallyハッシュに追加され、追加後の項目とその個数の組み合わせを返しています。

メモ

  • #tallyメソッドにはデフォルト値付きのハッシュも渡せます。配列に既に存在するキーはデフォルトのハッシュ値を無視し、配列に存在しないキーはnilの代わりにそのデフォルト値を返します。
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally(Hash.new 5)
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally[:ruler]
# => 5
  • #tallyメソッドにはProcも渡せます。配列内にキーが存在する場合はProcを無視し、キーが存在しない場合はProcが実行されます。
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally(Hash.new { puts "Proc called" })
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally[:ruler]
Proc called
# => nil
  • 配列に存在しないキーは、結果のハッシュとマージされます。たとえば、以下ではfirst_namelast_nameが配列orderにない場合にマージされています。
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally({ first_name: "Sam", last_name: "Example" }) # 可能
# => { first_name: "Sam", last_name: "Example", pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

配列内に存在するキーは、整数型の値だけを取る必要があります。そうでない場合はTypeErrorになります。

order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally({ pencil: "colored" }) # できない
# => TypeError: wrong argument type String (expected Integer)

関連記事

Ruby 3.1にArray#intersect?メソッドが追加(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。