Advanced Enumerable

Reviewed & published by Brayan K

Enumerable is the module that gives every Ruby collection its powerful iteration methods, and the advanced ones — reduce, group_by, chunk_while, and friends — let you transform and summarize data in a single expressive line.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Building on the each and map you already know, you'll collapse, classify, and reshape collections with the methods professionals reach for daily.

What You'll Learn in This Lesson

1️⃣ reduce / inject — Collapse to One Value

reduce threads an accumulator through every element, returning a single result. Give it a starting value and a block, or a shorthand symbol like :+ for simple operators. Whatever the block returns becomes the accumulator for the next element.

# reduce / inject: collapse a collection into ONE value
sum = [1, 2, 3, 4].reduce(0) { |acc, n| acc + n }   # acc starts at 0
puts sum                                            # 10

# A symbol shortcut works for simple operators:
puts [1, 2, 3, 4].reduce(:+)        # 10
puts [1, 2, 3, 4].reduce(1, :*)     # 24 (product, starting at 1)

# Build any value — here the longest word:
words = %w[cat elephant dog hippopotamus]
longest = words.reduce { |best, w| w.length > best.length ? w : best }
puts longest                        # hippopotamus

2️⃣ Classifying: group_by, partition, tally

group_by sorts elements into a hash of buckets keyed by the block's result; partition is the two-bucket special case returning [matches, rest]; and tally counts how many times each element appears.

# group_by, partition, and tally classify a collection
nums = (1..10).to_a

# group_by: build a hash of key => [matching items]
p nums.group_by { |n| n.even? ? :even : :odd }

# partition: split into two arrays [matches, rest]
evens, odds = nums.partition(&:even?)
p evens
p odds

# tally: count occurrences (Ruby 2.7+)
votes = %w[yes no yes yes no maybe]
p votes.tally

3️⃣ Building & Ordering: each_with_object, sort_by, chunk_while

each_with_object builds up a mutable container without juggling return values, flat_map maps then flattens one level, the _by methods order by a computed key, and chunk_while groups consecutive runs that satisfy a condition.

# each_with_object, flat_map, sort_by, min_by/max_by, chunk_while
# each_with_object: build up a collection without a separate accumulator return
counts = %w[a b a c a b].each_with_object(Hash.new(0)) do |letter, memo|
  memo[letter] += 1
end
p counts

# flat_map: map then flatten one level
p [[1, 2], [3, 4]].flat_map { |pair| pair }

# sort_by / min_by / max_by use a computed key
people = [{ name: "Ann", age: 30 }, { name: "Bo", age: 22 }, { name: "Cy", age: 41 }]
p people.sort_by { |h| h[:age] }.map { |h| h[:name] }
puts people.min_by { |h| h[:age] }[:name]   # youngest
puts people.max_by { |h| h[:age] }[:name]   # oldest

# chunk_while: group consecutive runs that satisfy a condition
p [1, 2, 4, 5, 7].chunk_while { |a, b| b == a + 1 }.to_a

Your turn. Fill in each ___ blank with the right method, then run it.

# 🎯 YOUR TURN — fill in the blanks, then run with: ruby totals.rb

prices = [4.5, 2.0, 9.25, 3.75]
total = prices.___(0, :+)            # 👉 collapse to a single sum (start at 0)
puts "Total: #{total}"

scores = [88, 92, 75, 60, 95]
passing, failing = scores.___ { |s| s >= 70 }   # 👉 split into [matches, rest]
puts "Passing: #{passing.length}, Failing: #{failing.length}"

# ✅ Expected output:
#    Total: 19.5
#    Passing: 4, Failing: 1

Mini-Challenge: Word-Frequency Report

Combine tally, max_by, and group_by to summarize a sentence. Run with ruby wordstats.rb.

# 🎯 MINI-CHALLENGE: a tiny word-frequency report.
# Save as wordstats.rb and run with: ruby wordstats.rb
#
# text = "the cat sat on the mat the cat ran"
# 1. Split into words and count them with tally.
# 2. Find the most common word with max_by on the count.
# 3. Group the UNIQUE words by their length with group_by(&:length).
#
# ✅ Example output:
#    {"the"=>3, "cat"=>2, "sat"=>1, "on"=>1, "mat"=>1, "ran"=>1}
#    Most common: the (3)
#    {3=>["the", "cat", "sat", "mat", "ran"], 2=>["on"]}

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Advanced Enumerable

GoalMethod
Collapse to one valuereduce(:+)
Bucket by keygroup_by { ... }
Two-way splitpartition { ... }
Count occurrencestally
Build a containereach_with_object(h)
Order by a keysort_by , min_by , max_by

🎉 Lesson Complete!

Practice quiz

What does `[1, 2, 3, 4].reduce(:+)` return?

  • 24
  • [1, 2, 3, 4]
  • 10
  • 4

Answer: 10. reduce(:+) sums every element: 1+2+3+4 = 10.

What does `[1, 2, 3, 4].reduce(1, :*)` return?

  • 24
  • 10
  • 0
  • 11

Answer: 24. Starting at 1 and multiplying gives the product 1*1*2*3*4 = 24.

What does group_by return?

  • A two-element array
  • A single value
  • A sorted array
  • A Hash of key => array of matching items

Answer: A Hash of key => array of matching items. group_by builds a Hash mapping each block result to the array of elements that produced it.

partition is the special case of grouping into how many buckets?

  • One
  • Exactly two ([matches, rest])
  • Three
  • Any number

Answer: Exactly two ([matches, rest]). partition returns exactly two arrays: [matches, non_matches] based on a true/false block.

What does `%w[yes no yes].tally` return?

  • {"yes"=>2, "no"=>1}
  • {"yes"=>1, "no"=>1}
  • ["yes", "no"]
  • 3

Answer: {"yes"=>2, "no"=>1}. tally counts occurrences of each element: yes appears twice, no once.

Why prefer each_with_object over reduce when building a Hash?

  • It is faster to type
  • It deeply flattens
  • It passes the same object each time and ignores the block's return
  • It sorts the keys

Answer: It passes the same object each time and ignores the block's return. each_with_object hands you the same object every iteration and ignores the block's return, so you just mutate it.

What does `people.max_by { |p| p[:age] }` return?

  • The largest age value
  • The whole person with the largest age
  • true/false
  • A sorted array

Answer: The whole person with the largest age. max_by returns the element itself, ordered by the computed key — the whole person, not just the age.

How deeply does flat_map flatten?

  • All levels
  • Two levels
  • It does not flatten
  • Exactly one level

Answer: Exactly one level. flat_map maps then flattens just one level; use flatten for deeper nesting.

What does `[1, 2, 4, 5, 7].chunk_while { |a, b| b == a + 1 }.to_a` produce?

  • [[1, 2, 4, 5, 7]]
  • [[1, 2], [4, 5], [7]]
  • [[1], [2], [4], [5], [7]]
  • [1, 2, 4, 5, 7]

Answer: [[1, 2], [4, 5], [7]]. chunk_while groups consecutive runs where each next value is one more than the previous.

When building a counter with each_with_object, what does `Hash.new(0)` provide?

  • A frozen hash
  • An empty array default
  • A default of 0 for every missing key
  • A KeyError on miss

Answer: A default of 0 for every missing key. Hash.new(0) gives missing keys a default of 0, so memo[key] += 1 works on first sight.

Continue this course

Frequently asked questions

What's the difference between reduce, each, and map?

each runs a block for its side effects and returns the original collection; map transforms each element and returns a new array of the same size; reduce (alias inject) collapses the whole collection down to a SINGLE value by threading an accumulator through every element. So [1,2,3].map { |n| n*2 } gives [2,4,6], but [1,2,3].reduce(:+) gives 6. Reach for reduce whenever the answer is one thing — a sum, a product, a maximum, or a hash you're building up.

When should I use each_with_object instead of reduce?

Both build a result, but they differ in what the block must return. reduce uses whatever the block returns as the next accumulator, so you must remember to end each block with the memo. each_with_object passes the SAME object every time and ignores the block's return value, so you just mutate it and move on — perfect when building up a Hash or Array. For accumulating into a mutable container, each_with_object(Hash.new(0)) is clearer and less error-prone than reduce.

What does group_by return, and how is it different from partition?

group_by returns a Hash that maps each distinct block result to an array of the elements that produced it, so you can split a collection into any number of named buckets. partition is the special case of exactly two buckets: it returns a two-element array [matches, non_matches] based on a true/false block. Use group_by when categories are open-ended (by length, by first letter, by status) and partition when it's a simple yes/no split.

How do sort_by and max_by differ from sort and max?

sort and max compare the elements themselves using <=>. The _by variants let you supply a block that computes a sort key for each element, and they compare those keys instead. So people.max_by { |p| p[:age] } returns the whole person with the largest age, not just the age. sort_by is also faster than sort { |a,b| ... } for expensive keys because it computes each key once (the Schwartzian transform). Use the _by forms whenever you're ordering objects by some derived attribute.