Skip to main content
โšก Calmops

Loops and Iteration in Ruby: A Complete Guide

Introduction

Ruby offers a rich set of iteration methods that go far beyond traditional for loops. Most Ruby developers prefer iterator methods over while or for loops because they’re more expressive, safer (no off-by-one errors), and integrate naturally with blocks.

Fixed-Count Iteration

times

Integer#times iterates a fixed number of times, yielding the current index (0-based):

4.times { |i| puts i }
# => 0
# => 1
# => 2
# => 3

4.times.to_a
# => [0, 1, 2, 3]

# Without the index
3.times { puts "Hello" }

upto

Integer#upto iterates from the receiver up to the argument (inclusive):

1.upto(4) { |i| puts i }
# => 1
# => 2
# => 3
# => 4

1.upto(4).to_a
# => [1, 2, 3, 4]

downto

Integer#downto iterates in reverse:

5.downto(1) { |i| print "#{i} " }
# => 5 4 3 2 1

step

Numeric#step iterates with a custom step size:

1.step(10, 2) { |i| print "#{i} " }
# => 1 3 5 7 9

0.0.step(1.0, 0.25) { |f| print "#{f} " }
# => 0.0 0.25 0.5 0.75 1.0

Range Iteration

Inclusive range (..)

(1..4).to_a
# => [1, 2, 3, 4]

(1..5).each { |i| puts i }

('a'..'e').to_a
# => ["a", "b", "c", "d", "e"]

Exclusive range (…)

(1...4).to_a
# => [1, 2, 3]  (excludes 4)

(0...array.length).each { |i| puts array[i] }

Array Iteration

each

The most common iterator โ€” yields each element:

fruits = ["apple", "banana", "cherry"]

fruits.each { |fruit| puts fruit }

# With index
fruits.each_with_index do |fruit, i|
  puts "#{i}: #{fruit}"
end
# => 0: apple
# => 1: banana
# => 2: cherry

# each_with_object โ€” accumulate into an object
result = fruits.each_with_object({}) do |fruit, hash|
  hash[fruit] = fruit.length
end
# => {"apple"=>5, "banana"=>6, "cherry"=>6}

map / collect

Transforms each element, returns a new array:

numbers = [1, 2, 3, 4, 5]

squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]

names = ["alice", "bob", "charlie"]
names.map(&:capitalize)
# => ["Alice", "Bob", "Charlie"]

select / filter

Returns elements where the block is truthy:

numbers = [1, 2, 3, 4, 5, 6]

evens = numbers.select { |n| n.even? }
# => [2, 4, 6]

odds = numbers.reject { |n| n.even? }
# => [1, 3, 5]

reduce / inject

Accumulates a result across all elements:

numbers = [1, 2, 3, 4, 5]

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

product = numbers.reduce(:*)
# => 120

# Build a hash from an array
words = ["hello", "world", "ruby"]
lengths = words.reduce({}) { |h, w| h.merge(w => w.length) }
# => {"hello"=>5, "world"=>5, "ruby"=>4}

each_slice and each_cons

(1..10).each_slice(3) { |s| p s }
# => [1, 2, 3]
# => [4, 5, 6]
# => [7, 8, 9]
# => [10]

(1..5).each_cons(3) { |c| p c }
# => [1, 2, 3]
# => [2, 3, 4]
# => [3, 4, 5]

Hash Iteration

scores = { alice: 95, bob: 87, charlie: 92 }

scores.each { |name, score| puts "#{name}: #{score}" }

# Map to a new hash
doubled = scores.transform_values { |v| v * 2 }
# => {alice: 190, bob: 174, charlie: 184}

# Filter
passing = scores.select { |_, score| score >= 90 }
# => {alice: 95, charlie: 92}

Conditional Loops

while

Runs while the condition is true:

i = 0
while i < 5
  puts i
  i += 1
end

# Inline form
i = 0
i += 1 while i < 5
puts i  # => 5

until

Runs until the condition becomes true (opposite of while):

i = 0
until i >= 5
  puts i
  i += 1
end

# Inline form
i = 0
i += 1 until i >= 5

loop

Infinite loop โ€” use break to exit:

i = 0
loop do
  break if i >= 5
  puts i
  i += 1
end

# Common pattern: loop with break on condition
result = loop do
  input = gets.chomp
  break input if input =~ /\A\d+\z/
  puts "Please enter a number"
end

for…in

Ruby has for...in but it’s rarely used โ€” each is preferred:

# Rarely used
for i in 1..5
  puts i
end

# Preferred
(1..5).each { |i| puts i }

The key difference: for doesn’t create a new scope for the block variable, while each does.

Controlling Loop Flow

next (like continue)

Skip to the next iteration:

(1..10).each do |i|
  next if i.even?
  puts i
end
# => 1 3 5 7 9

break

Exit the loop early, optionally returning a value:

result = [1, 2, 3, 4, 5].each do |i|
  break "found #{i}" if i == 3
end
puts result  # => "found 3"

redo

Restart the current iteration (use carefully):

attempts = 0
[1, 2, 3].each do |i|
  attempts += 1
  redo if attempts < 2 && i == 1  # retry first element once
  puts "#{i} (attempt #{attempts})"
end

Lazy Iteration

For large or infinite sequences, use lazy to avoid computing all values upfront:

# Without lazy: generates all 1M numbers first
(1..1_000_000).select { |n| n.odd? }.first(5)

# With lazy: stops as soon as 5 are found
(1..Float::INFINITY).lazy.select { |n| n.odd? }.first(5)
# => [1, 3, 5, 7, 9]

# Infinite Fibonacci sequence
fibs = Enumerator.new do |y|
  a, b = 0, 1
  loop { y << a; a, b = b, a + b }
end

fibs.lazy.select { |n| n.even? }.first(5)
# => [0, 2, 8, 34, 144]

Choosing the Right Iterator

Use case Method
Fixed count n.times
Range of numbers (a..b).each or a.upto(b)
Transform array map
Filter array select / reject
Accumulate value reduce / inject
Iterate with index each_with_index
Accumulate into object each_with_object
Infinite loop loop
Conditional loop while / until
Large/infinite sequences lazy

Resources

Comments