Ruby Enumerable — each map select reduce group_by and chunk Explained
In this tutorial, you will learn about Ruby Enumerable. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Enumerable provides 50+ collection methods by defining a single each method, including map for transformation, select for filtering, reduce for accumulation, and group_by for categorization.
What You'll Learn
- Using each and its variations for iteration
- Transforming collections with map and flat_map
- Filtering with select, reject, and partition
- Accumulating with reduce and each_with_object
- Grouping with group_by, chunk, and slice_by
Why It Matters
Enumerable is Ruby's most powerful module. Durga Antivirus Pro uses Enumerable methods to process scan results, filter threat patterns, and aggregate log data. Doda Browser uses map and reduce for bookmark processing and history analysis. Mastering Enumerable replaces 90% of manual loops with cleaner, safer code.
Real-World Use
Processing log files with map/select chains, grouping database records by attributes, computing statistics with reduce, chunking large datasets for batch processing — all built on Enumerable.
flowchart LR
A["Enumerable"] --> B["each"]
B --> C["map/select"]
C --> D["reduce"]
D --> E["group_by"]
E --> F["Lazy"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
The Foundation: each
Every Enumerable method is built on each. Define it once and get everything:
[1, 2, 3].each { |n| puts n * 2 }
# 2
# 4
# 6
# With index
["a", "b", "c"].each_with_index do |item, index|
puts "#{index}: #{item}"
end
# 0: a
# 1: b
# 2: c
each and Friends
# each_cons — consecutive chunks
[1, 2, 3, 4].each_cons(2) { |pair| puts pair.inspect }
# [1, 2]
# [2, 3]
# [3, 4]
# each_slice — fixed-size slices
[1, 2, 3, 4, 5].each_slice(2) { |slice| puts slice.inspect }
# [1, 2]
# [3, 4]
# [5]
# each_with_object — each with accumulator
["a", "b", "a", "c"].each_with_object(Hash.new(0)) do |item, counts|
counts[item] += 1
end
# => {"a" => 2, "b" => 1, "c" => 1}
Transformation: map and flat_map
map transforms each element:
nums = [1, 2, 3, 4, 5]
squared = nums.map { |n| n * n }
puts squared.inspect # [1, 4, 9, 16, 25]
words = ["hello", "world"]
upcased = words.map(&:upcase)
puts upcased.inspect # ["HELLO", "WORLD"]
temps_c = [0, 25, 100]
temps_f = temps_c.map { |c| c * 9.0 / 5 + 32 }
puts temps_f.inspect # [32.0, 77.0, 212.0]
flat_map for Nested Collections
data = [[1, 2], [3, 4], [5, 6]]
# map gives nested array
puts data.map { |arr| arr.map { |n| n * 2 } }.inspect
# [[2, 4], [6, 8], [10, 12]]
# flat_map flattens one level
puts data.flat_map { |arr| arr.map { |n| n * 2 } }.inspect
# [2, 4, 6, 8, 10, 12]
# Real-world: extracting attributes from nested structures
users = [
{ name: "Alice", tags: ["ruby", "rails"] },
{ name: "Bob", tags: ["python", "django"] }
]
all_tags = users.flat_map { |u| u[:tags] }
puts all_tags.inspect # ["ruby", "rails", "python", "django"]
Filtering: select, reject, and partition
numbers = [1, 2, 3, 4, 5, 6]
evens = numbers.select { |n| n.even? }
puts evens.inspect # [2, 4, 6]
odds = numbers.reject { |n| n.even? }
puts odds.inspect # [1, 3, 5]
# partition — both in one pass
evens, odds = numbers.partition { |n| n.even? }
puts evens.inspect # [2, 4, 6]
puts odds.inspect # [1, 3, 5]
# Real-world: filter log entries
logs = [
{ level: "ERROR", msg: "Timeout" },
{ level: "INFO", msg: "Started" },
{ level: "ERROR", msg: "Memory" },
{ level: "WARN", msg: "Disk" }
]
errors = logs.select { |log| log[:level] == "ERROR" }
puts errors.map { |e| e[:msg] }.inspect # ["Timeout", "Memory"]
Accumulation: reduce
reduce (also called inject) accumulates a value:
# Sum all numbers
sum = [1, 2, 3, 4, 5].reduce(0) { |acc, n| acc + n }
puts sum # 15
# Same with symbol shorthand
puts [1, 2, 3, 4, 5].reduce(:+) # 15
# Product
puts [1, 2, 3, 4].reduce(:*) # 24
# Find maximum
puts [3, 7, 2, 9, 1].reduce { |max, n| n > max ? n : max } # 9
# Building a hash
words = ["cat", "dog", "cat", "bird", "dog", "cat"]
counts = words.reduce(Hash.new(0)) { |acc, word| acc[word] += 1; acc }
puts counts.inspect # {"cat" => 3, "dog" => 2, "bird" => 1}
reduce vs each_with_object
# reduce requires returning the accumulator
result = [1, 2, 3].reduce({}) { |acc, n| acc[n] = n * 2; acc }
# each_with_object doesn't need return
result = [1, 2, 3].each_with_object({}) { |n, acc| acc[n] = n * 2 }
Grouping: group_by
people = [
{ name: "Alice", city: "NYC" },
{ name: "Bob", city: "SF" },
{ name: "Charlie", city: "NYC" },
{ name: "Diana", city: "SF" }
]
by_city = people.group_by { |p| p[:city] }
puts by_city.keys.inspect # ["NYC", "SF"]
puts by_city["NYC"].size # 2
# Group by first letter
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
by_letter = words.group_by { |w| w[0] }
puts by_letter.transform_values(&:size).inspect
# {"a" => 2, "b" => 2, "c" => 1}
chunk and slice_by
chunk groups consecutive elements:
data = [1, 1, 2, 2, 2, 3, 1, 1, 4]
data.chunk { |n| n }.each do |value, chunk|
puts "#{value}: #{chunk.inspect}"
end
# 1: [1, 1]
# 2: [2, 2, 2]
# 3: [3]
# 1: [1, 1]
# 4: [4]
# chunk with condition
numbers = [1, 2, 3, 4, 5, 6]
numbers.chunk { |n| n.even? }.each do |even, chunk|
puts "#{even ? 'even' : 'odd'}: #{chunk.inspect}"
end
# odd: [1]
# even: [2]
# odd: [3]
# even: [4]
# odd: [5]
# even: [6]
Sorting and Ordering
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
puts numbers.sort.inspect # [1, 1, 2, 3, 4, 5, 6, 9]
puts numbers.sort.reverse.inspect # [9, 6, 5, 4, 3, 2, 1, 1]
puts numbers.max # 9
puts numbers.min # 1
# Custom sort
people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
sorted = people.sort_by { |p| p[:age] }
puts sorted.map { |p| p[:name] }.inspect # ["Bob", "Alice", "Charlie"]
# Top N
puts numbers.max(3).inspect # [9, 6, 5]
puts numbers.min(3).inspect # [1, 1, 2]
Lazy Enumerables
For large or infinite collections, use lazy:
# Without lazy — computes entire array
result = (1..100_000).select { |n| n.even? }.first(5)
puts result.inspect # [2, 4, 6, 8, 10]
# With lazy — computes only what's needed
result = (1..Float::INFINITY).lazy
.select { |n| n.even? }
.map { |n| n * 2 }
.first(5)
puts result.inspect # [4, 8, 12, 16, 20]
# Lazy infinite Fibonacci
fib = Enumerator.new do |y|
a, b = 0, 1
loop { y << a; a, b = b, a + b }
end
puts fib.lazy.select(&:even?).first(6).inspect
# [0, 2, 8, 34, 144, 610]
Truthiness and grep
# grep — pattern match by ===
strings = ["hello", 42, :symbol, "world", 3.14]
puts strings.grep(String).inspect # ["hello", "world"]
puts strings.grep(Integer).inspect # [42]
# grep with ranges
numbers = [1, 10, 100, 1000]
puts numbers.grep(10..100).inspect # [10, 100]
# grep with regex
words = ["cat", "caterpillar", "dog", "category"]
puts words.grep(/^cat/).inspect # ["cat", "caterpillar", "category"]
Custom Enumerable Classes
class Roulette
include Enumerable
def each
loop { yield rand(1..36) }
end
end
spin = Roulette.new
puts spin.first(5).inspect # [17, 3, 28, 9, 14] (random)
puts spin.lazy.select(&:even?).first(3).inspect
Common Mistakes
1. Modifying Collections During Iteration
# Bad
arr = [1, 2, 3, 4]
arr.each { |n| arr.delete(n) if n.even? }
# Good — use reject
arr = [1, 2, 3, 4]
arr.reject!(&:even?)
2. Using each When map Would Be Better
# Bad — manual accumulation
result = []
[1, 2, 3].each { |n| result << n * 2 }
# Good — map
result = [1, 2, 3].map { |n| n * 2 }
3. Forgetting reduce Returns the Accumulator
# Wrong
result = [1, 2, 3].reduce({}) { |acc, n| acc[n] = n * 2 }
# Returns 6 (last assignment), not the hash!
# Correct
result = [1, 2, 3].reduce({}) { |acc, n| acc.tap { |a| a[n] = n * 2 } }
# Or use each_with_object
result = [1, 2, 3].each_with_object({}) { |n, acc| acc[n] = n * 2 }
4. Not Using Lazy for Infinite Sequences
# Will run forever
# (1..Float::INFINITY).select(&:even?).first(10)
# Correct — lazy
(1..Float::INFINITY).lazy.select(&:even?).first(10)
5. Overlooking each_with_object vs reduce
# reduce — must return accumulator
[1, 2, 3].reduce({}) { |acc, n| acc[n] = n.to_s; acc }
# each_with_object — no return needed
[1, 2, 3].each_with_object({}) { |n, acc| acc[n] = n.to_s }
Practice Questions
1. What is the contract for including Enumerable?
Define each that yields elements one at a time. Enumerable provides map, select, reduce, group_by, and 50+ other methods based on each.
2. What's the difference between map and flat_map?
map returns an array with the same structure. flat_map maps then flattens one level, useful for nested collections.
3. When should you use lazy?
For large or infinite collections where you only need a subset of results. Lazy avoids computing intermediate arrays.
4. How does reduce work with a symbol argument?
reduce(:+) calls the + method on each element, accumulating the result. This works with any binary method (:*, :-, etc.).
Challenge: Write a method that takes an array of transactions (hashes with amount and category) and returns a summary hash with total per category, the average Transaction amount, and the top 3 largest transactions.
Solution
def summarize_transactions(transactions)
{
totals: transactions
.group_by { |t| t[:category] }
.transform_values { |txns| txns.sum { |t| t[:amount] } },
average: transactions.map { |t| t[:amount] }.then { |a| a.sum / a.size.to_f },
top3: transactions
.sort_by { |t| -t[:amount] }
.first(3)
.map { |t| { t[:category] => t[:amount] } }
}
end
txns = [
{ category: "food", amount: 25 },
{ category: "food", amount: 40 },
{ category: "transport", amount: 15 },
{ category: "food", amount: 35 },
{ category: "entertainment", amount: 50 },
{ category: "transport", amount: 20 }
]
result = summarize_transactions(txns)
puts result.inspect
Expected output:
{:totals=>{"food"=>100, "transport"=>35, "entertainment"=>50}, :average=>30.833333333333332, :top3=>[{:entertainment=>50}, {:food=>40}, {:food=>35}]}
FAQ
{{< faq question="What's the difference between each and map?" >}}
each iterates and returns the original collection. map iterates and returns a new array of transformed values. Use each for side effects, map for transformations.
{{< /faq >}}
{{< faq question="Can I chain Enumerable methods?" >}}
Yes. [1,2,3].map { |n| n * 2 }.select(&:even?).reduce(:+) works and is idiomatic. Each method returns an array that the next method operates on.
{{< /faq >}}
{{< faq question="What is the Symbol#to_proc trick?" >}}
&:method_name converts a symbol to a proc that calls that method on each element. [1,2,3].map(&:to_s) is shorthand for [1,2,3].map { |n| n.to_s }.
{{< /faq >}}
{{< faq question="How many methods does Enumerable provide?" >}} Over 50, including map, select, reject, reduce, group_by, partition, all?, any?, none?, one?, count, sort, sort_by, max, min, minmax, chunk, slice_by, grep, detect, find, find_index, zip, cycle, inject, each_with_index, each_with_object, etc. {{< /faq >}}
{{< faq question="Is Enumerable faster than each?" >}} Enumerable methods are generally as fast as manual each loops. The Ruby C implementation optimizes common methods like map and select. Clarity and safety are the primary benefits. {{< /faq >}}
Try It Yourself
# enumerable_demo.rb
data = [
{ name: "Widget A", price: 10, quantity: 100 },
{ name: "Widget B", price: 25, quantity: 50 },
{ name: "Widget C", price: 5, quantity: 200 },
{ name: "Widget D", price: 50, quantity: 20 }
]
# Total inventory value
total_value = data.sum { |item| item[:price] * item[:quantity] }
puts "Total inventory value: $#{total_value}"
# Products sorted by value
by_value = data.sort_by { |item| -item[:price] * item[:quantity] }
by_value.each do |item|
value = item[:price] * item[:quantity]
puts "#{item[:name]}: $#{value}"
end
# Group by price tier
tiers = data.group_by { |item| item[:price] < 20 ? :budget : :premium }
puts "Budget: #{tiers[:budget].map { |i| i[:name] }.inspect}"
puts "Premium: #{tiers[:premium].map { |i| i[:name] }.inspect}"
# Average price
avg = data.map { |i| i[:price] }.then { |p| p.sum / p.size.to_f }
puts "Average price: $#{avg.round(2)}"
What's Next
Now that you've mastered Enumerable, explore Ruby's Date, Time, and DateTime classes for temporal data handling.
| Topic | Description | Link |
|---|---|---|
| Ruby Date and Time | DateTime, Time, Date Parsing | {{< ref "22-date-time" >}} |
| Ruby Marshal and Serialization | Object serialization | {{< ref "23-marshal-serialization" >}} |
| Python itertools | Compare Python's iteration tools | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro