Enumerable#each_cons
The each_cons method groups consecutive elements together and iterates over each group. Unlike each_slice which takes elements in order from the start, each_cons always uses consecutive elements with no gaps.
That makes it a natural fit for sliding windows, pairwise comparisons, and checks that depend on neighboring values. You can use it on arrays, ranges, and any enumerable that yields ordered items.
How it works
each_cons(n) takes n consecutive elements at a time:
collection.each_cons(n) { |group| block }
Each iteration gives you a slice of n consecutive elements. The last group may have fewer than n elements if the collection length isn’t evenly divisible.
That overlap is what makes the method useful for neighbor-based logic. Each group carries just enough context to compare one value with the next, which is often clearer than pulling values out by index.
Basic usage
Process pairs of consecutive elements:
[1, 2, 3, 4, 5].each_cons(2) { |pair| p pair }
# => [1, 2]
# => [2, 3]
# => [3, 4]
# => [4, 5]
Triple groups:
A window of three creates overlapping triples that let you compare each element against both its neighbors at once. This is essential for peak detection, inflection-point analysis, and any check that requires left and right context. Each triple overlaps the previous one by two elements.
[1, 2, 3, 4, 5].each_cons(3) { |group| p group }
# => [1, 2, 3]
# => [2, 3, 4]
# => [3, 4, 5]
These short examples are worth keeping in mind because they show the shape of the output very plainly. Once you know the output always moves one step at a time, the method becomes a good fit for comparisons and rolling calculations.
Sliding window algorithms
Moving averages
Calculate a 3-element moving average:
values = [10, 20, 30, 40, 50]
averages = values.each_cons(3).map { |window| window.sum.to_f / window.size }
# => [20.0, 30.0, 40.0]
Rate of change
Find the difference between consecutive elements:
each_cons(2) is the natural fit for computing step-by-step differences. The block destructures each pair into |a, b|, letting you subtract the earlier value from the later one in a single expression. This avoids the index arithmetic that a manual for loop would require.
prices = [100, 105, 103, 110]
changes = prices.each_cons(2).map { |a, b| b - a }
# => [5, -2, 7]
Finding peaks
Detect when a value is greater than both neighbors:
Peak detection needs a window of three: the middle element is compared against its left and right neighbors. The block destructures as |prev, curr, next_| so the condition curr > prev && curr > next_ reads naturally. The select call keeps only the triples where the middle value is a local maximum.
readings = [1, 3, 2, 5, 4, 6]
peaks = readings.each_cons(3).select { |prev, curr, next_| curr > prev && curr > next_ }
# => [[1, 3, 2], [2, 5, 4]]
# Peak values: 3, 5
The pattern is easy to extend when you need a wider window or a stricter check. The important part is that each_cons keeps the neighboring values together, so the comparison stays local and easy to read.
Pairwise comparisons
Compare every consecutive pair:
words = ["cat", "dog", "bird", "fish"]
words.each_cons(2) { |a, b| puts "#{a} -> #{b}" }
# cat -> dog
# dog -> bird
# bird -> fish
Sequential analysis
Finding consecutive sequences
each_cons(2) paired with chunk_while identifies runs of consecutive integers by checking whether each pair differs by exactly 1. The chunk_while method groups adjacent pairs that satisfy the condition, producing sub-arrays of uninterrupted sequences without any manual boundary tracking or index arithmetic.
numbers = [1, 2, 3, 5, 6, 7, 9]
sequences = numbers.each_cons(2).chunk_while { |a, b| b == a + 1 }.to_a
# => [[1, 2, 3], [5, 6, 7], [9]]
Validating ordered data
Check if a list is sorted:
each_cons(2).all? checks every adjacent pair and short-circuits at the first violation. For a sorted ascending check, the condition is a <= b. This pattern is cleaner than a manual loop with an early return and works for any monotonic property — descending order, non-decreasing, or strict inequality.
def sorted?(arr)
arr.each_cons(2).all? { |a, b| a <= b }
end
sorted?([1, 2, 3, 4]) # => true
sorted?([1, 3, 2, 4]) # => false
Running differences
Pairwise iteration with each_cons(2) creates a natural bridge between any element and the one after it. You can apply this pattern to adjacency checks, sentence boundary detection, or any task where the relationship between consecutive items matters more than absolute index position in the collection.
values = [100, 102, 105, 110]
diffs = values.each_cons(2).map { |a, b| [a, b, b - a] }
# => [[100, 102, 2], [102, 105, 3], [105, 110, 5]]
Window size considerations
Small windows
Window size of 2 (pairs):
A window of two produces n - 1 groups from a collection of n elements. Each group contains two consecutive items, and the groups overlap by one element. This is the most common window size and the basis for pairwise comparisons throughout the examples above.
[1, 2, 3].each_cons(2).to_a
# => [[1, 2], [2, 3]]
Large windows
A window of four over ten consecutive integers yields seven groups — each drops the oldest element and picks up the newest. The formula is always collection_size - window_size + 1. Larger windows are useful for trend detection, smoothing, and pattern recognition where more context is needed per step.
(1..10).each_cons(4).to_a
# => [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9], [7, 8, 9, 10]]
Window larger than collection
When the requested window size exceeds the collection length, each_cons yields nothing — no partial or padded groups are produced. This is the correct behaviour: if you ask for groups of five from a three-element array, the answer is “no groups exist.”
[1, 2, 3].each_cons(5).to_a
# => [] - no groups of 5
This return shape is easy to reason about: if the window cannot be filled, nothing is yielded. That keeps validation code honest, because it does not invent partial groups that were never present.
Practical Examples
Comparing adjacent list items
A window size of two is the most common configuration and produces exactly length - 1 groups. Every element except the last appears as the first member of a pair. This symmetric behavior makes the method easy to reason about when spanning a whole collection.
tasks = ["wake up", "brush teeth", "eat breakfast", "go to work"]
tasks.each_cons(2) { |prev, curr| puts "After #{prev}: do #{curr}" }
# After wake up: do brush teeth
# After brush teeth: do eat breakfast
# After eat breakfast: do go to work
Polynomial coefficients
each_cons(2) on a coefficient array produces adjacent pairs, which you can use to compute ratios between consecutive terms. For the expansion of (x - 1)^3, the coefficient list [1, -3, 3, -1] gives the ratios shown below, which can help verify polynomial identities or detect patterns in the term relationships.
coefficients = [1, -3, 3, -1] # (x - 1)^3
coefficients.each_cons(2).map { |a, b| b / a.to_f }
# => [-3.0, -1.0, -0.3333333333333333]
Text analysis
Find consecutive capital letters:
Scanning characters in pairs with each_cons(2) and a regex check identifies runs of uppercase letters. Both characters in the pair must match /[A-Z]/ for the pair to be selected. This pattern generalises to any adjacent-character property: vowel sequences, punctuation clusters, or digit runs.
text = "TheQuickBrownFox"
caps = text.chars.each_cons(2).select { |a, b| a =~ /[A-Z]/ && b =~ /[A-Z]/ }
# => [["Q", "B"], ["B", "F"]]
Comparison with related methods
| Method | Behavior |
|---|---|
each_cons | Consecutive elements, overlapping groups |
each_slice | Non-overlapping groups from start |
each_chunk | Groups based on block return value |
combination | All possible n-element combinations |
permutation | All possible orderings |
Listing adjacent items helps you build a narrative from a flat sequence of steps. The block receives prev and curr bindings for each pair, so the body reads like a natural-language instruction for what to do between tasks.
[1, 2, 3].each_cons(2).to_a # => [[1, 2], [2, 3]] - consecutive
[1, 2, 3].each_slice(2).to_a # => [[1, 2], [3]] - non-overlapping
[1, 2, 3].combination(2).to_a # => [[1, 2], [1, 3], [2, 3]] - all combos
Return value
Returns an Enumerator if no block is given:
When called without a block, each_cons returns an Enumerator. You can store it, pass it as an argument, or chain it with other enumerator methods like with_index. Calling to_a on the enumerator materialises all the consecutive groups at once.
enum = [1, 2, 3, 4].each_cons(2)
# => #<Enumerator: [1, 2, 3, 4]:each_cons(2)>
enum.to_a
# => [[1, 2], [2, 3], [3, 4]]
Edge cases
Empty collection:
An empty collection produces an empty result — there are no elements to group, so each_cons yields nothing. This is true for any window size and any collection type. The method never invents data or pads incomplete groups.
[].each_cons(2).to_a
# => []
Single element:
A collection with only one element also returns an empty result — a window of two cannot be formed from a single item. This is consistent with the general rule: each_cons(n) yields collection_size - n + 1 groups, which is zero when n > collection_size.
[1].each_cons(2).to_a
# => []
Nil values are included:
each_cons treats nil as a regular element — it appears in groups alongside non-nil values without any special handling. The method groups by position only and does not inspect or filter element values, so nil passes through the same sliding window as any other object would.
[1, nil, 3].each_cons(2).to_a
# => [[1, nil], [nil, 3]]
For many small algorithms, each_cons reads more clearly than indexing by hand. The method names the pattern directly, which makes the code easier to review and less likely to break when the collection changes size.
See Also
- Enumerable#each_slice - non-overlapping consecutive groups
- Enumerable#chunk - group elements by block return value
- Enumerable#each_with_object - iterate with accumulator
- Array#combination - all possible combinations
- Array#zip - combine arrays element-wise