Rails: Group results by week (using group_by)

The Enumerable class in Rails contains a method named ‘group_by’. This method is pure magic for a developer’s point of view. I’ll give you a simple example that shows the power of group_by.

Let’s say you have a table ‘posts’ containing blog posts. Now, you normally show these chronologically a few at a time. Nothing special there. For some special overview page, you want to group your posts by week.

With normal ActiveRecord operations this would be quite an elaborate task. But with group_by from Enumerable, it becomes child’s play.

First of all, in the controller, just get all the posts you need. In this case, all of them:

Controller:

def list
  @posts = Post.find :all
end

As you can see, I perform no ordering or whatsoever here.

Now, in your view you normally would iterate over all posts like this:

< %= render :partial => 'post', :collection => @posts %>

But, as I said, we want to group the posts by week. To make life easy, I add a method to the Post class that returns the week number in which a post was written:

Model Post:

def week
  self.created_at.strftime('%W')
end

Now, the magic will happen in our view:

< % @posts.group_by(&:week).each do |week, posts| %>
  <div id="week">
    <h2>Week < %= week %></h2>
    < %= render :partial => 'post', :collection => @posts %>
  </div>
< % end %>

Let me explain the above. We specify that we want to call group_by for @posts. But we need to say how we want to group these posts. By specifying &:week we tell group_by that we want to group by the result of the week attribute of every post. This is the attribute we specified earlier in the model.

Well, when the grouping is done we create a block that will handle every group of items. We extract ‘week’ and ‘posts’ here. ‘week’ contains the week number and ‘posts’ all the posts for that week.

As normal, we can now show the week number and iterate over the posts.

Sorting groups

The result of group_by is not guaranteed to be ordered in any way. Simply call ’sort’ before each and you’re set:

@posts.group_by(&:week).sort.each do |week, posts|

Mostly, you’ll find that the posts for every group are not sorted either. With the example above I think it’s easy to figure out how to do that now. (hint: .sort)

  • Twitter
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • DZone
  • Technorati
  • E-mail this story to a friend!

15 Comments so far

  1. brian on April 2nd, 2007

    Great post!!! This was exactly what I was looking for. Thank you!

  2. Duncan Gough on April 30th, 2007

    Wouldn’t it be better to offload that work to the database, though?

  3. juco on July 8th, 2007

    How exactly would you sort the posts inside the sorted weeks? I am doing something similar in my app but can’t figure out how to sort the records inside the group.

  4. Ariejan on July 8th, 2007

    @juco: You could do it like this:

    render :partial => ‘post’, :collection => @posts.sort {|a,b| a.title < => b.title}

    See Ruby’s sort documentation on how to sort your @posts.

  5. skwasha on October 10th, 2007

    So, I can get the groups listed separately… Now let’s say I want to re-render only one of the partials (say by column header sorting on a diff property). Is this possible? How would you go about it?

  6. Ariejan de Vroom on October 11th, 2007

    @skwasha: You could give every week a seperate id: instead of <div id=”week”>, you’d use <div id=”week-<%= week %>”>

    When you want to update your group, just select the appropriate data from you database (using conditions on your model like find_all_for_week or something. And then, using RJS, replace the div:

    render :update do |page|
    page.replace_html “week-#{week}”, :partial => ‘your group partial’
    end

  7. [...] Rails: Group results by week – This came in handy last night when I needed to do multilevel reporting in a Rails view. [...]

  8. garg on April 27th, 2008

    Hello,

    What goes in side the partial? When I wrote this in rails 2.0.2, it said that it couldn’t find the _post.html.erb partial to render. I created that and placed some queries inside but it just posted everything once for each week.

    Thanks!

  9. Christian on May 8th, 2008

    I’m having the same issue, I get it broken up nicely by week, but each post is listed under each week. ie 3 weeks = triple the posts.

  10. Christian on May 8th, 2008

    Ah ha! I found that (for me at least) this:

    Week
    ‘post’, :collection => @posts %>

    should read:

    Week
    ‘post’, :collection => posts %>

    *the @ symbol should be removed from ‘:collection => @posts’

    This solved my problem of getting repeating posts. It seems that the collection should be of the local variable posts rather than the instance variable of @post (which contains all of the entries in the database)

  11. Victor on November 9th, 2008

    How about if you want to group_by one attribute but you want to sort the resultant groups by another attribute of the object?

    i.e., I am grouping by the distance_of_time_in_words value of the release_date attribute but when I sort the resultant hash, everything’s sorted alphabetically (i.e. instead of “6 Days “, “5 Weeks”, “1 Month”, “4 Months” I get “1 Month”, “4 Months”, “5 Weeks”, “6 Days”)

    Here’s some of my code:
    Controller:
    @items = Category.all_category_items.group_by {|i| distance_of_time_in_words(Time.now, i.release_date)}

    View:

    This gives me the alphabetical order.

    How do I sort by the first @item.release_date in the group instead of the distance_of_time_in_words(Time.now, i.release_date) but still group by distance_of_time_in_words(Time.now, i.release_date)?

  12. Victor on November 9th, 2008

    Whoops, forgot to add the view code:

  13. Victor on November 9th, 2008

    @items.keys.sort.each do |release_date|

  14. Tillmann Carlos Bielefeld on March 12th, 2009

    Thank you, man! eventize.de now groups events. Cheers!

  15. Shaun on May 12th, 2009

    Great post! Only problem is your color scheme. Purple on gray is not readable.

Leave a Reply