rubyguides

Array#chunk_while

arr.chunk_while { |elt_before, elt_after| bool }

What chunk_while does

Array#chunk_while walks an array pairwise and groups elements into runs. The block decides whether two neighbors belong in the same chunk: return truthy and the run stays open, return false (or nil) and a new chunk starts at the next element. The method is actually defined on Enumerable, so anything that mixes in Enumerable, such as Array, Range, Hash, and your own classes, gets it for free.

The canonical example, taken from the official Ruby docs:

a = [1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21]
b = a.chunk_while { |i, j| i + 1 == j }
p b
# => #<Enumerator: [1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21]:chunk_while({...})>
p b.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]

chunk_while itself returns an Enumerator, not an array. That is the source of a lot of confusion: if you p it without to_a, you see #<Enumerator: ...>. Call to_a (or map, each, etc.) to materialize the chunks as an array of arrays.

Signature and return value

arr.chunk_while { |elt_before, elt_after| bool } → an_enumerator
arr.chunk_while                                  → an_enumerator

The call always returns an Enumerator. With a block, the block is stored on the enumerator; without a block, you get the same object ready to chain methods on. The block is called size - 1 times, so the last element is never passed in, because there is no neighbor after it to compare against.

Omitting the block gives you a chainable Enumerator:

[1, 2, 3, 4].chunk_while
# => #<Enumerator: [1, 2, 3, 4]:chunk_while>

A common pattern is to compute run-lengths from a sorted list: pass the consecutive predicate, then map each chunk to a [first, length] pair. Because the Enumerator is chainable, this stays a one-liner and avoids materializing the chunked array form until you actually need it.

[1, 2, 3, 5, 6, 9]
  .chunk_while { |a, b| a + 1 == b }
  .map { |run| [run.first, run.length] }
# => [[1, 3], [5, 2], [9, 1]]

Predicate polarity: true keeps, false splits

The block’s truthy value means “these two belong together.” That is the opposite of how slice_when reads, and Enumerable#slice_when does the same job with the polarity inverted. If you find yourself writing chunk_while { |a, b| !condition(a, b) }, swap to slice_when instead: it expresses the same intent without the negation.

A non-decreasing run, where each chunk is a maximal stretch of numbers that never decreases:

a = [0, 9, 2, 2, 3, 2, 7, 5, 9, 5]
p a.chunk_while { |i, j| i <= j }.to_a
# => [[0, 9], [2, 2, 3], [2, 7], [5, 9], [5]]

Grouping by parity works the same way: the block returns true while the current pair shares a parity, then false at the moment the parity flips. This is a tidy pattern for separating alternating runs without writing a manual state machine.

a = [7, 5, 9, 2, 0, 7, 9, 4, 2, 0]
p a.chunk_while { |i, j| i.even? == j.even? }.to_a
# => [[7, 5, 9], [2, 0], [7, 9], [4, 2, 0]]

The block’s return value is truthy-tested, not strictly true/false. 1, :keep, and "yes" all keep a chunk open; only false and nil start a new chunk.

Edge cases

A few inputs worth knowing about, all derived from the “block is called size - 1 times” rule:

[].chunk_while { |a, b| true }.to_a
# => []

[1].chunk_while { |a, b| a == b }.to_a
# => [[1]]

[1, 2, 3].chunk_while { |a, b| true }.to_a
# => [[1, 2, 3]]

[1, 2, 3].chunk_while { |a, b| false }.to_a
# => [[1], [2], [3]]

[1, 1, 1].chunk_while { |a, b| a == b }.to_a
# => [[1, 1, 1]]

The single-element case is the one that surprises people: the block is never called, so the lone element is always its own chunk. The same rule explains the empty-array result: no block calls, no boundaries, one empty chunk list.

chunk_while does not clone, copy, or transform the elements. Each chunk is an array of references to the originals, in their original order. If you need to mutate per-chunk, do it inside the chained map rather than on the input array.

chunk_while vs each_slice vs chunk

Three methods, three different jobs:

  • each_slice(n) makes fixed-size chunks of n. No block predicate and no decision-making, so every group has exactly n elements (except possibly the last).
  • chunk { |el| key } groups by a key derived from each element. The key can be anything; a key change starts a new chunk. Use it when each element carries the information you want to group by.
  • chunk_while { |a, b| keep? } groups adjacent elements by a pairwise predicate. The decision is local to the boundary between two neighbors, so it cannot “remember” anything beyond that boundary.

Reach for each_slice when the chunk size is constant (batches of 100, pages of 25, etc.). Reach for chunk when each element carries a key you can extract directly. Reach for chunk_while when a chunk’s membership depends on its neighbors. Typical examples are sorted runs, parity groups, and “consecutive days” windows in a date list.

See also