rubyguides

Array#slice_when

slice_when { |elt_before, elt_after| ... }

Array#slice_when returns an array of arrays, splitting the receiver between every pair of adjacent elements for which the given block returns a truthy value. It is defined on Enumerable and Array inherits it unchanged. The method was added in Ruby 2.3, alongside the closely related chunk_while, slice_before, and slice_after.

The canonical use case is breaking a sorted sequence into runs:

[1, 2, 3, 5, 6, 8, 9, 10].slice_when { |a, b| b - a > 1 }.to_a
# => [[1, 2, 3], [5, 6], [8, 9, 10]]

The block b - a > 1 is true exactly when there is a gap, so a new slice starts at every gap.

Signature

slice_when { |elt_before, elt_after| ... }   # => Array
slice_when                                  # => Enumerator
FormReturns
With a blockArray of Array slices. Eager.
Without a blockEnumerator. Lazy until iterated or converted with .to_a.

The block receives two arguments, the element on the left of the candidate split and the element on the right. It is called array.length - 1 times, so the block never runs for arrays of length 0 or 1.

How it works

The block is a boundary detector. When it returns a truthy value for a pair (a, b), a new slice starts before b, and a and b end up in different slices. Returning falsy keeps the two elements together in the current slice. Once a boundary is inserted, the next comparison starts from the element after the boundary, so there is no lookahead beyond one step.

Walking through [1, 2, 3, 5, 6, 8] with the block b - a > 1:

PairBlock resultEffect
(1, 2)1 > 1 is falsekeep together
(2, 3)1 > 1 is falsekeep together
(3, 5)2 > 1 is truecut before 5
(5, 6)1 > 1 is falsekeep together
(6, 8)2 > 1 is truecut before 8

Result: [[1, 2, 3], [5, 6], [8]].

Examples

Group by sign change

<=> against 0 returns -1, 0, or 1, so comparing the signs of two adjacent numbers is a clean way to detect a sign boundary:

[1, 2, 3, -2, -3, 4, 5, -1].slice_when { |a, b| (a <=> 0) != (b <=> 0) }.to_a
# => [[1, 2, 3], [-2, -3], [4, 5], [-1]]

Split a string by case alternation

Array#chars returns an array of single-character strings, so slice_when operates on the character array directly. The block decides where each new slice starts based on the relationship between two neighboring characters, which makes case alternation straightforward to express as a transition condition between adjacent letters.

"aBcDeFgH".chars.slice_when { |a, b| a == a.upcase && b == b.downcase }.to_a
# => [["a", "B"], ["c", "D"], ["e", "F"], ["g", "H"]]

Group by time gap

Any object that responds to a comparison works with slice_when, including structs, hashes, and custom classes. The example below groups a log of timestamped events by gaps larger than 60 seconds, a common pattern for spotting activity bursts separated by quiet stretches in a stream of records.

events = [
  { time:   0, msg: "ping" },
  { time:  10, msg: "ping" },
  { time:  20, msg: "ping" },
  { time: 200, msg: "ping" },
  { time: 210, msg: "ping" }
]

events.slice_when { |prev, curr| curr[:time] - prev[:time] > 60 }.to_a
# => [[{time: 0, ...}, {time: 10, ...}, {time: 20, ...}],
#     [{time: 200, ...}, {time: 210, ...}]]

Without a block, returns an Enumerator

When called without a block, slice_when returns an Enumerator that you can iterate or convert to an array later. The lazy form is handy for chaining slice_when with other Enumerable methods, or for deferring the work until the slices are actually needed. The receiver array itself stays untouched until you call .to_a or otherwise iterate the enumerator.

e = [1, 2, 4, 5].slice_when { |a, b| b - a > 1 }
e.class        # => Enumerator
e.to_a         # => [[1, 2], [4, 5]]

slice_when vs chunk_while

slice_when and chunk_while are the two halves of the same idea, and they are exact inverses of each other. slice_when inserts a slice boundary whenever the block returns true, while chunk_while groups elements together only as long as the block returns true. Reading both versions of the same predicate side by side makes the inverse framing easy to verify.

[1, 2, 3, 5, 6, 8].slice_when  { |a, b| b - a > 1 }.to_a
# => [[1, 2, 3], [5, 6], [8]]

[1, 2, 3, 5, 6, 8].chunk_while { |a, b| (b - a) <= 1 }.to_a
# => [[1, 2, 3], [5, 6], [8]]

A useful framing: slice_when answers “should a cut go here?”, while chunk_while answers “should these stay together?” Pick the one whose question reads more naturally for the problem at hand.

Common Mistakes

  • Truthy means split, not “include”. A truthy return value inserts a slice boundary between the two elements. It never causes an element to be dropped.
  • No block returns Enumerator, not Array. If you write result = arr.slice_when, call .to_a (or iterate) before you try to use result as an array.
  • Different from Array#slice. Array#slice (no _when) is the index, range, and offset accessor. Same prefix, unrelated method.
  • Different from slice_before and slice_after. Those split at elements matching a pattern or sentinel. slice_when splits purely on the relationship between two neighbors, so it is the right tool for runs, monotonicity changes, and sign changes.
  • Does not mutate. The receiver is unchanged. A new array of new inner arrays is returned.
  • Edge cases. Empty arrays return []. A one-element array returns [[element]] and the block never runs. A block that always returns false returns one big slice, and a block that always returns true returns one-element slices for every element.

See Also