3

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, and each_with_object appropriately;
  • 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] }
worked_examples.md
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]
end
worked_examples.md
ruby

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] }
cheatsheet.md
ruby

Build a hash incrementally

invoices.each_with_object({}) do |invoice, totals|
  currency = invoice[:currency]
  totals[currency] ||= 0
  totals[currency] += invoice[:amount]
end
cheatsheet.md
ruby

Selection guide

  • map: produce a new collection of transformed values
  • select: keep matching items
  • sum: total numeric values
  • max_by: choose the item with the highest computed value
  • each_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 ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md before looking at the code.
  3. Study worked_examples.md and identify the business question each method answers.
  4. Keep cheatsheet.md open while reading example.rb.
  5. Complete the basic exercise.
  6. Complete the advanced exercise and explain why each_with_object is 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 sum clearer than incrementing a running variable manually?
  • Why does each_with_object fit grouped totals better than map?
  • 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
en/topic_03_collections_enumerable/example.rb
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
en/topic_03_collections_enumerable/basic_exercise.rb
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
en/topic_03_collections_enumerable/adv_exercise.rb
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
en/topic_03_collections_enumerable/answer_basic_exercise.rb
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
en/topic_03_collections_enumerable/answer_adv_exercise.rb
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
en/topic_03_collections_enumerable/tests/topic_03_collections_enumerable_spec.rb
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}")"
en/topic_03_collections_enumerable/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.