How to display a collection grouped by an attribute value in Rails

Flashback time. We are in 1999 and you are coding in ASP thinking that this language is the future. You use ADODB recordsets to iterate over collections. You have a recordset containing some records from a “quotes” table. The “quotes” table contains a author column (varchar) and a body column (varchar). Now you want to display the results grouped by author name so it looks like this :
Georges Brassens
– Les filles quand ça dit “je t’aime”, c’est comme un second baptême
– Aucune idée sur terre n’est digne d’un trépas
Billie Holiday
– Don’t threaten me with love, baby. Let’s just go walking in the rain.
– I never hurt nobody but myself and that’s nobody’s business but my own.
Assuming your recordset is ordered by author, you do something like this (remember, we’re in 1999) :
[vb]<TABLE style="color:fushia;font-style:MSONormal generated=frontpage">
<%while not objRS.eof %>
<%if(current_author <> objRS("author"))%>
<h1><%=objRS("author")%></h1>
<%end if%>
<Tr><td><%=objRS("body")</td></TR>
<%current_author = objRS("author")%>
<%objRS.MoveNext%>
<%loop%>
<%objRS.close%>
</TABLE>[/vb]
Ok welcome back in 2008. ASP is dead. You are coding in rails and you want to do the same thing. How will you do it? Storing the author name in a buffer variable like in 1999? Not too sure about it.
This is a job for Enumerable#group_by (Enumerable is a module that is mixed in the Array class)

all_quotes = Quote.find(:all)
@authors = all_quotes.group_by(&:author)

Enumerable#group_by will create different sets of quotes based on the “author” values. Why the “&” sign? It’s because group_by expects a block. I could have done it this way : @authors = all_quotes.group_by{|quote| quote.author}
So @authors is now a hash that will look like this:
{“George Brassens” => [#<Quote id:131 …>, #<Quote id:331 …>], “Billie Holiday” => [#<Quote id:111 …>, #<Quote id:911 …>] }
Now you can iterate over it like this :

@authors.each_pair do |author_name, quotes|
  <h1><%=author_name%></h1>
  <%quotes.each do |quote| %>
    - <%=quote.body%><br />
  <%end%>
<%end%>

One more thing to note : @authors is a hash, and hashes cannot be ordered. If you want to display quotes by sorted author name, you could do this :

@authors.keys.sort.each do |author_name|
  <h1><%=author_name%></h1>
  <%@authors[author_name].each do |quote|%>
    - <%=quote.body%><br />
  <%end%>
<%end%>

Note : Enumerable#group_by does not exist in ruby 1.8, it only exists in Rails. The method will be in ruby 1.9 however.

6 thoughts on “How to display a collection grouped by an attribute value in Rails

  1. It is always interesting to learn new tricks but in this case I would consider more efficient to get the data ready to use straight from the DB rather than getting the same data and reorganize in memory. I would do something like this:
    @authors = Author.find :all, :include => :quotes, :order => :name
    this returns an array of authors and pre-load their quotes using two sql instances such as:
    SELECT * FROM `authors` ORDER BY name;
    SELECT `quotes`.* FROM `quotes` WHERE (`quotes`.author_id IN (1,2,3,…));
    in the view you just loop all authors and their quotes:

Leave a Reply

Your email address will not be published. Required fields are marked *