Skip to content

Ruby Loops

Loops are fundamental control structures in programming used to repeatedly execute a block of code until a specific condition is met. Ruby provides various loop statements and iterators that enable code to efficiently handle repetitive tasks. This chapter will introduce in detail the various loop statements in Ruby and how to use them.

🔄 while Loops

Basic while Loops

ruby
# Basic syntax: while condition do ... end
counter = 0

while counter < 5
  puts "Counter: #{counter}"
  counter += 1
end

# Output:
# Counter: 0
# Counter: 1
# Counter: 2
# Counter: 3
# Counter: 4

Single-Line while Loops

ruby
# Modifier form of while loop
counter = 0
puts "Counter: #{counter += 1}" while counter < 5

# Output:
# Counter: 1
# Counter: 2
# Counter: 3
# Counter: 4
# Counter: 5

while Loop Variations

ruby
# Use break to exit loop early
counter = 0

while true
  puts "Counter: #{counter}"
  counter += 1
  break if counter >= 5
end

# Use next to skip current iteration
counter = 0

while counter < 10
  counter += 1
  next if counter.even?  # Skip even numbers
  puts "Odd: #{counter}"
end

# Output:
# Odd: 1
# Odd: 3
# Odd: 5
# Odd: 7
# Odd: 9

🔁 until Loops

Basic until Loops

ruby
# until is the reverse form of while: until condition do ... end
counter = 0

until counter >= 5
  puts "Counter: #{counter}"
  counter += 1
end

# Equivalent to: while counter < 5

Single-Line until Loops

ruby
# Modifier form of until loop
counter = 0
puts "Counter: #{counter += 1}" until counter >= 5

# Output:
# Counter: 1
# Counter: 2
# Counter: 3
# Counter: 4
# Counter: 5

🔁 loop Loops

Basic loop Loops

ruby
# loop creates infinite loop, must use break to exit
counter = 0

loop do
  puts "Counter: #{counter}"
  counter += 1
  break if counter >= 5
end

loop Loop Applications

ruby
# Read user input until "quit" is entered
loop do
  print "Enter command (enter 'quit' to exit): "
  input = gets.chomp
  break if input == "quit"
  puts "You entered: #{input}"
end

# Retry mechanism
def fetch_data
  attempts = 0
  max_attempts = 3
  
  loop do
    begin
      # Simulate network request
      if rand > 0.3  # 70% success rate
        return "Data fetched successfully"
      else
        raise "Network error"
      end
    rescue => e
      attempts += 1
      if attempts >= max_attempts
        puts "Maximum retry attempts reached"
        return nil
      else
        puts "Retrying attempt #{attempts}..."
        sleep 1
      end
    end
  end
end

🔢 Numeric Loops

times Loops

ruby
# Integer's times method
5.times do |i|
  puts "Iteration #{i + 1}"
end

# Output:
# Iteration 1
# Iteration 2
# Iteration 3
# Iteration 4
# Iteration 5

# Single-line form
5.times { |i| puts "Counter: #{i}" }

# Without index parameter
5.times do
  puts "Hello!"
end

upto and downto Loops

ruby
# From current number to specified number
1.upto(5) do |i|
  puts "Counting up: #{i}"
end

# From current number to specified number (downward)
10.downto(5) do |i|
  puts "Counting down: #{i}"
end

# Single-line form
1.upto(3) { |i| puts "Number: #{i}" }
10.downto(8) { |i| puts "Countdown: #{i}" }

step Loops

ruby
# Loop with specified step
1.step(10, 2) do |i|
  puts "Step 2: #{i}"
end

# Output: 1, 3, 5, 7, 9

# Reverse step
10.step(1, -2) do |i|
  puts "Reverse step 2: #{i}"
end

# Output: 10, 8, 6, 4, 2

📦 Collection Iteration

each Iterator

ruby
# Array iteration
fruits = ["Apple", "Banana", "Orange"]

fruits.each do |fruit|
  puts "Fruit: #{fruit}"
end

# Hash iteration
person = {name: "Zhang San", age: 25, city: "Beijing"}

person.each do |key, value|
  puts "#{key}: #{value}"
end

# Single-line form
fruits.each { |fruit| puts fruit }

# Iterate only keys or values
person.each_key { |key| puts key }
person.each_value { |value| puts value }

each_with_index Iterator

ruby
# Get both element and index simultaneously
fruits = ["Apple", "Banana", "Orange"]

fruits.each_with_index do |fruit, index|
  puts "Fruit #{index + 1}: #{fruit}"
end

# Hash with each_with_index
person = {name: "Zhang San", age: 25, city: "Beijing"}

person.each_with_index do |(key, value), index|
  puts "Item #{index + 1}: #{key} = #{value}"
end

map/collect Iterator

ruby
# Transform array elements
numbers = [1, 2, 3, 4, 5]

# map returns new array
squared = numbers.map { |n| n * n }
puts squared.inspect  # [1, 4, 9, 16, 25]

# In-place modification using map!
numbers.map! { |n| n * 2 }
puts numbers.inspect  # [2, 4, 6, 8, 10]

# String processing example
names = ["Zhang San", "Li Si", "Wang Wu"]
capitalized = names.map { |name| name.upcase }
puts capitalized.inspect  # ["ZHANG SAN", "LI SI", "WANG WU"]

select/reject Iterator

ruby
# Filter elements
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Select even numbers
evens = numbers.select { |n| n.even? }
puts evens.inspect  # [2, 4, 6, 8, 10]

# Reject odd numbers (equivalent to selecting even)
evens = numbers.reject { |n| n.odd? }
puts evens.inspect  # [2, 4, 6, 8, 10]

# String filtering example
words = ["apple", "banana", "cherry", "date"]
long_words = words.select { |word| word.length > 5 }
puts long_words.inspect  # ["banana", "cherry"]

find/detect Iterator

ruby
# Find first matching element
numbers = [1, 3, 5, 8, 9, 12]

# Find first even number
first_even = numbers.find { |n| n.even? }
puts first_even  # 8

# Find first word longer than 5 characters
words = ["cat", "dog", "elephant", "bird"]
long_word = words.find { |word| word.length > 5 }
puts long_word  # elephant

# If not found, returns nil
no_match = numbers.find { |n| n > 20 }
puts no_match  # nil

# Provide default value
no_match_with_default = numbers.find(-> { "Not found" }) { |n| n > 20 }
puts no_match_with_default  # Not found

🔧 Advanced Iterators

reduce/inject Iterator

ruby
# Reduction operations
numbers = [1, 2, 3, 4, 5]

# Calculate sum
sum = numbers.reduce(0) { |acc, n| acc + n }
puts sum  # 15

# Shorthand form
sum = numbers.reduce(:+)
puts sum  # 15

# Calculate product
product = numbers.reduce(1) { |acc, n| acc * n }
puts product  # 120

# Find maximum
max = numbers.reduce { |max, n| n > max ? n : max }
puts max  # 5

# String concatenation
words = ["Hello", " ", "World", "!"]
sentence = words.reduce("") { |acc, word| acc + word }
puts sentence  # Hello World!

group_by Iterator

ruby
# Group by condition
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Group by parity
groups = numbers.group_by { |n| n.even? ? "Even" : "Odd" }
puts groups
# {"Odd"=>[1, 3, 5, 7, 9], "Even"=>[2, 4, 6, 8, 10]}

# Group strings by length
words = ["cat", "dog", "elephant", "bird", "ant"]
by_length = words.group_by { |word| word.length }
puts by_length
# {3=>["cat", "dog"], 8=>["elephant"], 4=>["bird"], 3=>["ant"]}
# Note: Same keys are merged, actual result is {3=>["cat", "dog", "ant"], 8=>["elephant"], 4=>["bird"]}

sort_by Iterator

ruby
# Sort by condition
students = [
  {name: "Zhang San", age: 20, score: 85},
  {name: "Li Si", age: 19, score: 92},
  {name: "Wang Wu", age: 21, score: 78}
]

# Sort by score
by_score = students.sort_by { |student| student[:score] }
puts by_score.map { |s| s[:name] }  # ["Wang Wu", "Zhang San", "Li Si"]

# Sort by age (descending)
by_age = students.sort_by { |student| -student[:age] }
puts by_age.map { |s| s[:name] }  # ["Wang Wu", "Zhang San", "Li Si"]

⚙️ Loop Control Statements

break Statement

ruby
# Exit loop early
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Find first number greater than 5
result = numbers.each do |n|
  if n > 5
    puts "Found first number greater than 5: #{n}"
    break n  # Return n as the result of each method
  end
end

puts "Result: #{result}"  # Result: 6

next Statement

ruby
# Skip current iteration
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Only process odd numbers
numbers.each do |n|
  next if n.even?  # Skip even numbers
  puts "Odd: #{n}"
end

# Output:
# Odd: 1
# Odd: 3
# Odd: 5
# Odd: 7
# Odd: 9

redo Statement

ruby
# Re-execute current iteration
counter = 0

3.times do |i|
  counter += 1
  puts "Iteration #{i + 1}, Counter: #{counter}"
  redo if counter < 3 && i == 1  # Re-execute on second iteration
end

# Output:
# Iteration 1, Counter: 1
# Iteration 2, Counter: 2
# Iteration 2, Counter: 3
# Iteration 3, Counter: 4

retry Statement

ruby
# Retry in exception handling
attempts = 0

begin
  attempts += 1
  puts "Attempt #{attempts}"
  
  # Simulate operation that may fail
  if attempts < 3
    raise "Operation failed"
  end
  
  puts "Operation successful"
rescue => e
  puts "Error: #{e.message}"
  retry if attempts < 5
  puts "Give up retrying"
end

🧪 Loop Practice Examples

Data Processing Pipeline

ruby
class DataProcessor
  def self.process(data)
    data
      .select { |item| item[:active] }           # Filter active items
      .sort_by { |item| -item[:priority] }       # Sort by priority
      .map { |item| transform_item(item) }       # Transform data
      .take(10)                                  # Take first 10 items
  end
  
  private
  
  def self.transform_item(item)
    {
      id: item[:id],
      name: item[:name].upcase,
      score: calculate_score(item),
      processed_at: Time.now
    }
  end
  
  def self.calculate_score(item)
    (item[:value] * item[:weight]).round(2)
  end
end

# Use data processing pipeline
raw_data = [
  {id: 1, name: "Project A", value: 10, weight: 2.5, priority: 1, active: true},
  {id: 2, name: "Project B", value: 15, weight: 1.8, priority: 3, active: true},
  {id: 3, name: "Project C", value: 8, weight: 3.2, priority: 2, active: false},
  {id: 4, name: "Project D", value: 12, weight: 2.1, priority: 4, active: true}
]

processed_data = DataProcessor.process(raw_data)
processed_data.each { |item| puts item }

File Processing Loop

ruby
class FileProcessor
  def self.process_files(directory, pattern = "*.txt")
    processed_count = 0
    error_count = 0
    
    Dir.glob(File.join(directory, pattern)).each_with_index do |file_path, index|
      begin
        puts "Processing file #{index + 1}: #{File.basename(file_path)}"
        
        # Read file content
        content = File.read(file_path, encoding: "UTF-8")
        
        # Process content
        processed_content = process_content(content)
        
        # Save processing result
        output_path = file_path.sub(/\.txt$/, "_processed.txt")
        File.write(output_path, processed_content)
        
        processed_count += 1
        puts "  ✓ Processing complete"
        
      rescue => e
        error_count += 1
        puts "  ✗ Processing failed: #{e.message}"
        next  # Continue processing next file
      end
    end
    
    puts "\nProcessing complete: #{processed_count} successful, #{error_count} failed"
  end
  
  private
  
  def self.process_content(content)
    # Example processing: Convert to uppercase and add timestamp
    processed = content.upcase
    timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    processed + "\n\nProcessing time: #{timestamp}"
  end
end

# Use file processor (needs actual files to run)
# FileProcessor.process_files("/path/to/directory")

User Input Validation Loop

ruby
class UserInputValidator
  def self.get_valid_input(prompt, validation_proc, error_message)
    loop do
      print prompt
      input = gets.chomp
      
      if validation_proc.call(input)
        return input
      else
        puts error_message
      end
    end
  end
  
  def self.get_number_in_range(min, max)
    get_valid_input(
      "Enter number between #{min} and #{max}: ",
      ->(input) { 
        begin
          num = Integer(input)
          num >= min && num <= max
        rescue ArgumentError
          false
        end
      },
      "Invalid input, please enter an integer between #{min} and #{max}"
    ).to_i
  end
  
  def self.get_email
    get_valid_input(
      "Enter email address: ",
      ->(input) { input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) },
      "Invalid email format"
    )
  end
  
  def self.get_choice(options)
    puts "Please select:"
    options.each_with_index do |option, index|
      puts "  #{index + 1}. #{option}"
    end
    
    choice = get_valid_input(
      "Enter selection (1-#{options.length}): ",
      ->(input) { 
        begin
          num = Integer(input)
          num >= 1 && num <= options.length
        rescue ArgumentError
          false
        end
      },
      "Please enter a valid selection"
    ).to_i
    
    options[choice - 1]
  end
end

# Use input validator (interactive example)
=begin
# Get age
age = UserInputValidator.get_number_in_range(1, 120)
puts "Your age is: #{age}"

# Get email
email = UserInputValidator.get_email
puts "Your email is: #{email}"

# Get selection
choices = ["Option A", "Option B", "Option C"]
selected = UserInputValidator.get_choice(choices)
puts "You selected: #{selected}"
=end

🎯 Loop Best Practices

1. Choose Appropriate Loop Method

ruby
# Good practice: Choose loop method based on scenario

# Known iteration count: Use times
5.times { |i| puts "Iteration #{i + 1}" }

# Numeric range: Use upto/downto
1.upto(10) { |i| puts i }

# Collection traversal: Use each
[1, 2, 3].each { |item| puts item }

# Data transformation: Use map
squared = [1, 2, 3].map { |n| n * n }

# Condition filtering: Use select
evens = [1, 2, 3, 4, 5].select(&:even?)

2. Avoid Infinite Loops

ruby
# Good practice: Ensure loop has exit condition
counter = 0
while counter < 10
  puts counter
  counter += 1  # Ensure counter increases
end

# Use iterators instead of manual loops
# Not recommended:
i = 0
while i < array.length
  process(array[i])
  i += 1
end

# Recommended:
array.each { |item| process(item) }

3. Use Loop Control Statements Reasonably

ruby
# Good practice: Clear control flow
def find_user(users, target_name)
  users.each do |user|
    return user if user[:name] == target_name
  end
  nil  # Not found
end

# Avoid complex nested controls
# Not recommended:
users.each do |user|
  if user[:active]
    posts.each do |post|
      if post[:author] == user[:name]
        if post[:published]
          # Complex processing logic
        end
      end
    end
  end
end

# Recommended: Break down complex logic
def process_user_posts(user, posts)
  return unless user[:active]
  
  user_posts = posts.select { |post| 
    post[:author] == user[:name] && post[:published] 
  }
  
  user_posts.each { |post| process_post(post) }
end

4. Performance Considerations

ruby
# Avoid repeated calculations in loops
# Not recommended:
array.each do |item|
  expensive_calculation = calculate_something()  # Calculated every time
  process(item, expensive_calculation)
end

# Recommended:
expensive_calculation = calculate_something()  # Calculated only once
array.each do |item|
  process(item, expensive_calculation)
end

# Use appropriate methods instead of manual loops
# Not recommended:
sum = 0
numbers.each { |n| sum += n }

# Recommended:
sum = numbers.sum

📚 Next Steps

After mastering Ruby loop statements, it is recommended to continue learning:

Continue your Ruby learning journey!

Content is for learning and research only.