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
- Collapsing a collection with reduce / inject
- Classifying with group_by, partition, and tally
- Building results with each_with_object
- Flattening one level with flat_map
- Ordering by a key with sort_by, min_by, max_by
- Grouping consecutive runs with chunk_while
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 # hippopotamus2️⃣ 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.tally3️⃣ 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_aYour 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: 1Mini-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 hereCommon Errors (and the fix)
- ❌ reduce block not returning the accumulator → ✅ make the memo the last line of the block.
- ❌ Using reduce to build a hash → ✅ prefer each_with_object(Hash.new); you just mutate it.
- ❌ Calling max when you meant max_by → ✅ max_by returns the element, ordered by a key.
- ❌ Expecting flat_map to flatten deeply → ✅ it flattens just one level; use flatten for more.
- ❌ Forgetting .to_a on chunk_while → ✅ it returns a lazy enumerator; realize it with .to_a.
Pro Tips
- 💡 tally beats a manual count loop: words.tally replaces a whole each-with-hash dance.
- 💡 group_by then transform: arr.group_by(&:odd?).transform_values(&:size) gives counts per group.
- 💡 sum exists too: for plain addition, [1,2,3].sum is even clearer than reduce(:+).
📋 Quick Reference — Advanced Enumerable
| Goal | Method |
|---|---|
| Collapse to one value | reduce(:+) |
| Bucket by key | group_by { ... } |
| Two-way split | partition { ... } |
| Count occurrences | tally |
| Build a container | each_with_object(h) |
| Order by a key | sort_by , min_by , max_by |
🎉 Lesson Complete!
- ✅ reduce/inject collapse a collection to one value
- ✅ group_by, partition, and tally classify data
- ✅ each_with_object builds a container cleanly
- ✅ sort_by/min_by/max_by order by a computed key
- ✅ flat_map and chunk_while reshape sequences
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
- Previous: String Methods
- Next: Struct & OpenStruct — Lightweight value objects with Struct, keyword_init, and Data.define
- Quick reference: Ruby cheat sheet
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.