Collections and Enumerable Thinking
Ruby source files are converted directly from the English source tree.
en/topic_03_collections_enumerable
Overview
Why this topic matters ๐ก
Enumerable is one of the most important Ruby capabilities for everyday programming. It encourages students to describe transformations and selections directly rather than managing indexes and mutable accumulators manually.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- choose
map,select,sum,max_by, andeach_with_objectappropriately; - explain the difference between transformation, filtering, and reduction;
- write collection code that reads declaratively;
- identify when manual loops are less expressive than Enumerable;
- explain why data-oriented Ruby code often feels denser but clearer than Java loops.
Assessment focus โ
Students should be able to justify the Enumerable method they choose, not only make the test pass.
Short Note
Many Ruby programmers feel the language becomes especially elegant when processing collections. Enumerable lets you name the kind of work directly:
- transform;
- filter;
- reduce;
- group;
- search.
That matters pedagogically because it shifts attention from "how do I loop?" to "what operation am I expressing?" This is a strong change in mindset for many Java developers.
Ruby beauty in this topic:
- collection methods align closely with intent;
- code often becomes shorter and more readable at the same time;
- chaining small operations can express complex transformations clearly.
Ruby caution in this topic:
- a long chain can become unreadable if names and intermediate ideas are unclear;
- not every problem should become a clever one-liner.
Reflection prompt:
- Which is easier to review in a pull request: a mutable loop or a named Enumerable pipeline?
Worked Examples
Example 1: Reporting totals ๐ก
Order and invoice reporting are excellent Ruby examples because they are common and data-heavy. Students can see immediately why collection methods feel natural.
orders.sum { |order| order[:amount] }ruby
This reads more like a business rule than a loop.
Example 2: Grouping money by currency ๐ก
Finance and commerce code often groups values by a shared key.
invoices.each_with_object({}) do |invoice, totals|
key = invoice[:currency]
totals[key] ||= 0
totals[key] += invoice[:amount]
endruby
Why this is useful:
- the accumulator is explicit;
- the code avoids index management;
- the result structure matches the reporting need directly.
Cheatsheet
Common Enumerable tools
orders.sum { |o| o[:amount] }
orders.select { |o| o[:amount] >= 100 }
orders.map { |o| o[:customer] }
items.max_by { |i| i[:score] }ruby
Build a hash incrementally
invoices.each_with_object({}) do |invoice, totals|
currency = invoice[:currency]
totals[currency] ||= 0
totals[currency] += invoice[:amount]
endruby
Selection guide
map: produce a new collection of transformed valuesselect: keep matching itemssum: total numeric valuesmax_by: choose the item with the highest computed valueeach_with_object: build a result structure explicitly
Study Guide
Topic purpose ๐ฏ
Learn to express collection work declaratively. This topic is central because it is where many students first feel Ruby's expressiveness rather than only noticing its syntax.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.mdbefore looking at the code. - Study
worked_examples.mdand identify the business question each method answers. - Keep
cheatsheet.mdopen while readingexample.rb. - Complete the basic exercise.
- Complete the advanced exercise and explain why
each_with_objectis a good fit.
What to notice ๐
- Enumerable methods often describe intent better than loops.
- The method name is part of the design vocabulary.
- A good accumulator shape reduces complexity later.
Reflection questions ๐ค
- Why is
sumclearer than incrementing a running variable manually? - Why does
each_with_objectfit grouped totals better thanmap? - When should you stop chaining and extract a named method?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_03_collections_enumerable
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.
class OrderReport
def total_amount(orders)
orders.sum { |o| o[:amount] }
end
def high_value_orders(orders, min: 100)
orders.select { |o| o[:amount] >= min }
end
def customer_names(orders)
orders.map { |o| o[:customer] }.uniq.sort
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_03_collections_enumerable
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_03_collections_enumerable_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.
class ScoreAnalyzer
def average_score(items)
return 0.0 if items.empty?
items.sum { |i| i[:score] }.to_f / items.size
end
def passing_names(items, threshold: 60)
items.select { |i| i[:score] >= threshold }.map { |i| i[:name] }
end
def top_scorer(items)
items.max_by { |i| i[:score] }&.dig(:name)
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_03_collections_enumerable
#
# Academic purpose:
# - Practice turning a reporting requirement into a declarative collection transformation.
# - Understand why Ruby developers reach for Enumerable when shaping business data.
#
# Real-world use case:
# - Multi-currency invoice reporting is common in billing, analytics, and finance tools.
# - Teams often need "totals by group" answers for dashboards or exports.
# - This is a realistic place where Ruby's collection methods feel powerful rather than decorative.
#
# Why Ruby is beautiful here:
# - `each_with_object` makes the accumulator explicit without boilerplate loop setup.
# - The code mirrors the business question: "group totals by currency".
# - The method stays close to the domain language.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Focus on why the chosen Enumerable style matches the reporting requirement.
# - Use tests in tests/topic_03_collections_enumerable_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can defend your use of Enumerable.
class InvoiceSummarizer
def group_total_by_currency(invoices)
invoices.each_with_object({}) do |invoice, totals|
key = invoice[:currency]
totals[key] ||= 0
totals[key] += invoice[:amount]
end
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_03_collections_enumerable
#
# Solution idea:
# - Choose the Enumerable method that matches the job:
# average -> `sum`
# pass list -> `select` + `map`
# top result -> `max_by`
# - The code reads better when each method expresses one kind of collection work.
class ScoreAnalyzer
def average_score(items)
return 0.0 if items.empty?
items.sum { |item| item[:score] }.to_f / items.size
end
def passing_names(items, threshold: 60)
items.select { |item| item[:score] >= threshold }.map { |item| item[:name] }
end
def top_scorer(items)
items.max_by { |item| item[:score] }&.dig(:name)
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_03_collections_enumerable
#
# Solution idea:
# - Build a hash accumulator explicitly with `each_with_object`.
# - Each invoice contributes its amount into the bucket for its currency.
# - This mirrors how grouped totals are computed in reporting code.
class InvoiceSummarizer
def group_total_by_currency(invoices)
invoices.each_with_object({}) do |invoice, totals|
currency = invoice[:currency]
totals[currency] ||= 0
totals[currency] += invoice[:amount]
end
end
end
Ruby course source
# This spec is your learning companion for topic_03_collections_enumerable.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_03_collections_enumerable" do
describe "EXAMPLE purpose: understand the reference implementation" do
it "builds report totals and names" do
orders = [
{ customer: "Ann", amount: 120 },
{ customer: "Bob", amount: 80 },
{ customer: "Ann", amount: 30 }
]
r = OrderReport.new
expect(r.total_amount(orders)).to eq(230)
expect(r.high_value_orders(orders, min: 80).size).to eq(2)
expect(r.customer_names(orders)).to eq(%w[Ann Bob])
end
end
describe "BASIC EXERCISE purpose: implement the comparable task" do
it "analyzes score data" do
s = ScoreAnalyzer.new
scores = [{ name: "A", score: 80 }, { name: "B", score: 60 }, { name: "C", score: 40 }]
expect(s.average_score(scores)).to eq(60.0)
expect(s.passing_names(scores, threshold: 60)).to eq(%w[A B])
expect(s.top_scorer(scores)).to eq("A")
end
end
describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
it "groups invoice totals by currency" do
i = InvoiceSummarizer.new
invoices = [{ currency: "USD", amount: 10 }, { currency: "THB", amount: 700 }, { currency: "USD", amount: 40 }]
expect(i.group_total_by_currency(invoices)).to eq({ "USD" => 50, "THB" => 700 })
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.