Meet Chunk and Partition, the Enumerators You're Not Using Enough Of

#chunk and #partition are two of my favorite Ruby enumerators. You don't need them often, but when you do, there's nothing else that quite hits the spot.

Let's look at #chunk first.

#chunk takes an array (or other enumerable object) and "chunks" the array's items into groups. It iterates over each item, passes it into a block, and items which return the same value from the block are grouped. Like so:

people = [
  { :house => 'Targaryen', :name => 'The Mother of Dragons' },
  { :house => 'Targaryen', :name => 'Aerys "The Mad King"' },
  { :house => 'Lannister', :name => 'Jaime "Kingslayer" Lannister' },
  { :house => 'Lannister', :name => 'Cersei Lannister' },
  { :house => 'Stark', :name => 'Jon Snow' },
  { :house => 'Stark', :name => 'Maester Luwin' },
  { :house => 'Stark', :name => 'Ned Start' }
]

people.chunk { |p| p[:house] }.each do |house, members|
  names = members.map { |m| m[:name] }
  puts "#{names.to_sentence} swear allegiance to House #{house}."
end

This outputs:

The Mother of Dragons and Aerys "The Mad King" swear allegiance to House Targaryen.
Jaime "Kingslayer" Lannister and Cersei Lannister swear allegiance to House Lannister.
Jon Snow, Maester Luwin, and Ned Start swear allegiance to House Stark.

It's important to note that chuck only groups consecutive elements. So where do I find this useful? In Rails views, specifically for reports.

Let's say a client asks you to create a simple report which outputs all the users who have joined his website, grouped by the month that they joined. #chunk allows you to do that easily.

In your controller -

@users = User.order('created_at desc')

In your view, you can now simply iterate over them.

%h1 User Monthly Breakdown

- @users.chunk { |u| u.created_at.strftime("%B %Y") }.each do |month, users|
  %h3= month
  %table.table
    %thead
      %tr
        %th Name
        %th Email

    - users.each do |user|
      %tr
        %td= user.name
        %td= user.email

You'll get a page with multiple tables, each table headed by the month (eg August 2013) and the users who joined in that month. Nice and easy.

(A close relative of #chunk is #slice_before, with the difference being that #slice_before can chunk based on a pattern.)

Chunk's Partner-in-Crime: Partition

Partition is similar to #chunk. It too groups items, but only into two groups: items for which the block returns true, and items for which it returns false.

people = [
  { :house => 'Lannister', :name => 'Jaime "Kingslayer" Lannister' },
  { :house => 'Lannister', :name => 'Cersei Lannister' },
  { :house => 'Stark', :name => 'Jon Snow' },
  { :house => 'Stark', :name => 'Maester Luwin' },
  { :house => 'Stark', :name => 'Ned Start' }
]

lannisters, starks = people.partition { |person| person[:house] == 'Lannister' }

And Now For Some Cousins

Two other useful grouping methods are provided by ActiveSupport.

#in_groups breaks an array into smaller groups. For example @users.in_groups(3) will break your @users array into three smaller arrays. This is very useful in view files when outputting a collection into a set number of columns.

#in_groups_of breaks an array into groups the size of the parameter you specify. For example @users.in_groups_of(3) will break your @users array into smaller arrays of three users each.

The thing to remember with both these methods is that they automatically "pad" the extra places with nils. For example, if your @users collection has 10 users, and you run @users.in_groups_of(3), you will end up with four smaller arrays. The first three will have three user objects each, but the last will have one user object and two nil objects.

The problem is that the nil objects will almost always choke your code. As you iterate over the arrays, the nils will throw NoMethodErrors. To avoid this, both #in_groups and #in_groups_of take a second parameter: if you specify false, as in @users.in_groups_of(3, false), the array will not be padded with nils.

(1..10).to_a.in_groups(3, false)
# => [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14], [15, 16, 17, 18, 19, 20]] 

(1..10).to_a.in_groups_of(3, false)
# => [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17, 18], [19, 20]] 

What is your favorite enumerator? Teach us in the comments.