rubyguides

Enumerable#chunk

The chunk method groups consecutive elements that return the same value from the block. It is ideal for processing runs of identical values, state changes, or categorized data without loading everything into memory first.

Enumerable#chunk is especially helpful when the order matters. Instead of gathering all matching items into one bucket, it keeps the original sequence intact and splits it into runs whenever the block output changes.

how it works

chunk passes each element to the block and groups consecutive elements with the same return value:

That means the block controls the boundaries between groups. If two adjacent elements produce the same result, they stay together; if the result changes, chunk starts a new group.

enumerable.chunk { |element| block_value }
# Returns an Enumerator of [block_value, elements_array] pairs

Basic Usage

Group consecutive even and odd numbers:

This simple case is a good way to see the rule in action. The result keeps the original order, but it breaks the sequence into smaller arrays whenever the block output flips between true and false.

[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk { |n| n.even? }.to_a
# => [[false, [3, 1]], [true, [4]], [false, [1, 5, 9]], [true, [2, 6]], [false, [5, 3, 5]]]

Practical Examples

The examples below move from tiny numeric sequences to logs and tabular data. Each one uses the same idea, but the surrounding context changes how you read the output and what you can do with it.

processing log files

Group consecutive log entries by level:

log_levels = [:info, :info, :warn, :error, :error, :info, :debug, :debug]

log_levels.chunk { |level| level }.each do |level, entries|
  puts "#{level}: #{entries.count} entries"
end
# Output:
# info: 2 entries
# warn: 1 entries
# error: 2 entries
# info: 1 entries
# debug: 2 entries

This pattern is useful when you want to preserve the original order but still summarize each run. In a log stream, for example, it can show how long a level stayed active before the next one appeared.

grouping consecutive letters

Find runs of consecutive characters in a string:

"aaabbbccdaa".chars.chunk(&:itself).map { |char, arr| [char, arr.length] }
# => [["a", 3], ["b", 3], ["c", 2], ["d", 1], ["a", 2]]

Grouping consecutive characters like this is handy when the input is really a sequence of runs, not a bag of unrelated values. The method keeps the original order intact, which makes the result easy to inspect or post-process.

processing CSV data

Group rows by a category column:

rows = [
  { category: "fruit", name: "apple" },
  { category: "fruit", name: "banana" },
  { category: "vegetable", name: "carrot" },
  { category: "fruit", name: "date" },
  { category: "vegetable", name: "eggplant" }
]

rows.chunk { |row| row[:category] }.each do |category, items|
  puts "#{category}: #{items.map { |i| i[:name] }.join(', ')}"
end
# Output:
# fruit: apple, banana
# vegetable: carrot
# fruit: date
# vegetable: eggplant

This style is useful when the category changes over time and you want one summary per stretch of related rows. You can still see where each run starts and ends, which would be lost if you grouped everything globally.

finding state changes

Track when a status changes in a sequence:

statuses = [:idle, :idle, :running, :running, :running, :complete, :idle, :idle]

statuses.chunk_while { |before, after| before == after }.each do |run|
  puts "Status: #{run.first}, Duration: #{run.length}"
end
# Output:
# Status: idle, Duration: 2
# Status: running, Duration: 3
# Status: complete, Duration: 1
# Status: idle, Duration: 2

State transitions are one of the clearest uses for chunk_while, because the block reads like the rule that keeps a run together. When the rule changes, the enumerator starts a fresh group automatically, and the resulting enumerator captures both the status name and its duration in one data structure.

grouping test results

Process test results in batches:

results = [:pass, :pass, :fail, :fail, :pass, :pass, :pass]

results.chunk { |r| r == :pass ? :passed : :failed }.each do |status, tests|
  puts "#{status}: #{tests.count} tests"
end
# Output:
# passed: 2 tests
# failed: 2 tests
# passed: 3 tests

This example shows that you can also use chunk to normalize similar values into a simpler label before grouping them. That is useful for reporting code where you want to count outcomes instead of inspecting each raw item.

using :underscore to skip elements

The special :underscore symbol tells chunk to skip that element entirely:

[1, 2, 3, :skip, 4, 5, 6, :skip, 7].chunk { |x| x == :skip ? :_skip : x.even? }.to_a
# => [[false, [1]], [true, [2]], [false, [3]], [true, [4, 6]], [false, [7]]]

This is useful for filtering out certain values while still grouping the rest. The :_underscore token works as a special signal to the enumerator, telling it to drop those elements from the output entirely without breaking the grouping of the surrounding items. You can think of it as an inline filtering step that happens at the same time as the grouping itself.

MethodWhat It Does
chunkGroups consecutive elements by block return value
group_byGroups all elements regardless of order/consecutive
slice_whenSplits when block returns true between elements
chunk_whileLike slice_when but creates Enumerator
# chunk - only groups consecutive elements
[1, 2, 1, 2].chunk { |x| x.odd? }.to_a
# => [[true, [1]], [false, [2]], [true, [1]], [false, [2]]]

# group_by - groups all matching elements together
[1, 2, 1, 2].group_by { |x| x.odd? }
# => {true => [1, 1], false => [2, 2]}

The difference between chunk and group_by comes down to whether order matters. chunk preserves the original sequence and only groups adjacent matches, while group_by collects all matching elements into a single bucket regardless of where they appear. Choosing between them depends on whether you are analyzing runs and transitions or simply categorizing items.

Return Value

Returns an Enumerator. Use to_a to get an array, or iterate directly:

That return type makes chunk flexible in chains. You can inspect the grouped pairs immediately, convert them to an array, or keep iterating as part of a larger pipeline.

[1, 2, 2, 3, 3, 3].chunk { |n| n }
# => #<Enumerator: [1, 2, 2, 3, 3, 3]:chunk>

[1, 2, 2, 3, 3, 3].chunk { |n| n }.each { |k, v| puts "#{k}: #{v}" }
# 1: [1]
# 2: [2, 2]
# 3: [3, 3, 3]

The enumerator form is nice when you want to keep chaining methods after chunk. You can inspect the result lazily first, then materialize it only when you actually need an array.

Edge Cases

The edge cases below are worth remembering because they show that chunk always preserves the structure implied by the block result. A run of identical values stays together, and a different value always starts a new group.

Empty collections return an empty enumerator:

[].chunk { |x| x }.to_a
# => []

An empty collection stays empty, which is usually what you want when you are processing a stream or a list that may not have any rows yet. The method does not raise an error or return a special sentinel value; it simply produces a result with nothing to iterate over. This makes it safe to use in pipelines where the input size is unknown, since the shape of the return value remains consistent even when there is no data.

Single element:

[42].chunk { |x| x }.to_a
# => [[42, [42]]]

A single element still produces one group, so the return shape remains consistent. That consistency makes the method easier to use in code that handles many different input sizes. When every piece of input belongs to the same category, you get exactly one pair, and that pair wraps all elements together inside a single array.

All same values:

[1, 1, 1, 1].chunk { |x| x }.to_a
# => [[1, [1, 1, 1, 1]]]

When every element matches the same block result, chunk gives you one run instead of splitting the data unnecessarily. That matches the mental model of “one stretch, one group.” The method does not waste effort splitting a homogeneous list into smaller pieces when there is no boundary to detect.

All different values:

[1, 2, 3, 4].chunk { |x| x }.to_a
# => [[1, [1]], [2, [2]], [3, [3]], [4, [4]]]

If every element is different, each one becomes its own group. That is still useful, because it shows the exact boundary where the block output changes.

See Also