Rails の Array と Emumerable 拡張

よく使うので、自分用にまとめ。

前提

以下、すべてのサンプルで Category 及び Entry というモデルを操作します。
持っている値は、以下のとおり。

  • Category (has many entries)
    • name => :string
  • Entry (belongs to category)
    • category_id => :integer
    • title => :string
    • body => :text
    • view_count => :integer
    • created_at => :datetime
    • updated_at => :datetime

Array

active_support\core_ext\array\groupng.rb を参照。バージョンは 2.0.2 準拠です。

in_groups_of

恐らくこれが一番よく使うメソッド。配列を指定された数値ごとのグループに分けてくれます。以下、サンプルで説明します。

[1, 2, 3, 4, 5, 6, 7].in_groups_of(3)
=> [[1, 2, 3], [4, 5, 6], [7, nil, nil]]

このように配列が分割され、不足分は nil が代入されます。
ブロックを使って操作する事も可能。

[1, 2, 3, 4, 5, 6, 7].in_groups_of(3) do |group|
  p group.join
end
"123"
"456"
"7"
=> nil

よくあるのが、ビューで使うケース。

@entries = Entry.find(:all)

このように find してきたエントリを、横に2つずつ並べる事になってしまった時。

<% unless @entries.blank? %>
  <table>
    <% @entries.in_groups_of(2) do |group| %>
      <tr>
      <% group.each do |e| %>
        <% if e %>
          <td>
            <h1><%= e.title %></h1>
            <p><%= e.body %></p>
          </td>
        <% else %>
          <td>&nbsp;</td>
        <% end %>
      <% end %>
      </tr>
    <% end %>
  </table>
<% end %>

html 的な是非はともかく、同じ処理を自分で行うとミスしやすいので、便利だと思います。
ちなみに blank? メソッドも Rails の拡張で、これはこれで便利なメソッドです。

split

こちらはあまり使った事がありません。条件に従って、配列を分割します。言葉では説明しづらいので、サンプルで。

[1, 2, 3, 4, 5].split(3)
=>[[1, 2], [4, 5]]

このように、指定された値は削除され、残った部分がグループ化されます。
これもブロックを使った条件指定が可能。

(1..10).to_a.split do |i|
  i % 3 == 0
end
=>[[1, 2], [4, 5], [7, 8], [10]]

find と組み合わせて使う場合はあまりない気がします(:conditions オプションで大体片付くはず)。何か特別な処理が必要な時に思い出すといいかもしれません。

Enumerable

active_support\core_ext\enumerable.rb 参照。こちらも 2.0.2 準拠。

group_by

これもグルーピングですが、個数ではなく条件によるグループ化を行います。

entries = Entry.find(:all)
# カテゴリ別にグループ化
@catregorized_entries = entries.group_by {|e| e.category_id}
# 年/月別でグループ化
@monthly_entries = entries.group_by {|e| e.created_at.strftime("%Y%m")}

サンプルで分かるように、かなり柔軟に条件指定ができるようです。グループ化した時の条件部分も表示可能。

<% @monthly_entries.each |term, entries| do %>
  <h1><%= term %></h1>
  <% entries.each do |e| %>
    <p><%= e.title %></p>
  <% end %>
<% end %>
sum

合計を求めます。
一番簡単なサンプル。

[1, 2, 3].sum
=> 6

ブロックを使う事もできます。実際には、ほとんどの場合こちらを使うと思います。

entries = Entry.find(:all)
catregorized_entries = entries.group_by {|e| e.category_id}

カテゴリ別でエントリを取得。これらのエントリの参照数が view_count で取得できるとして、合計値を表示します。

<% categorized_entries.each do |category, entries| %>
  <h1><%= category.name %>の全エントリの参照数</h1>
  <%= entries.sum {|e| e.view_count } %>
<% end %>

以上、コードとしては非常に無駄が多いですが、使い方は理解できると思います。
なお、デフォルトの場合空の配列への操作は 0 が返りますが、オーバーライド可能です。

[].sum {|i| i.amount}
=> 0
[].sum(Payment.new(0)) {|i| i.amount}
=> Payment.new(0)

ソースに書いてあったサンプルそのままで申し訳ないのですが、返り値を任意のオブジェクトにできるという事です。うまく使うと、コードの簡略化に役立つかもしれません。

index_by

group_by に似ていますが、こちらはハッシュを作ります。あるキーに複数の値が属する事になる場合、ひとつだけ(ソースを読む限り、一番最後に一致した値)が選ばれます。
使い方は group_by に似ています。

categories = Category.find(:all)
categories.index_by(&:name)
=> {"diary" => <Category ...>, "rails" => <Category ...>}
entries = Entry.find(:all)
entries.index_by {|e| e.created_at.strftime("%Y/%m/%d ") + e.title}
=> {"20080216 本日の日記" => <Entry ...>, "20080215 本日の日記" => <Entry ...>