Overview
Range iteration in Ruby provides methods for traversing sequences defined by Range objects. Ruby implements ranges as objects that represent intervals between two values, with built-in support for iteration through Enumerable module inclusion. The Range class defines start and end values with optional exclusion of the endpoint, enabling both finite and infinite sequence traversal.
Ruby ranges support iteration through multiple mechanisms: the each
method for basic traversal, step-based iteration for custom intervals, and lazy evaluation for memory-efficient processing. The iteration behavior depends on the range type, step value, and whether the endpoint is included or excluded from the sequence.
# Basic finite range iteration
(1..5).each { |n| puts n }
# Output: 1, 2, 3, 4, 5
# Infinite range with lazy evaluation
(1..).lazy.take(3).to_a
# => [1, 2, 3]
# Step-based iteration
(0..10).step(2) { |n| puts n }
# Output: 0, 2, 4, 6, 8, 10
Range objects implement iteration through the Enumerable mixin, inheriting methods like map
, select
, reduce
, and each_with_index
. The iteration mechanism calls the succ
method on range elements, requiring objects to respond to successor generation. Integer, Float, String, and Time objects support range iteration natively.
Basic Usage
Range iteration begins with the each
method, which traverses from the start value to the end value using the succ
method. Inclusive ranges (..
) include the endpoint, while exclusive ranges (...
) exclude it. The iteration stops when the current value equals or exceeds the end value for exclusive ranges, or after including the end value for inclusive ranges.
# Inclusive range iteration
(1..4).each { |i| print "#{i} " }
# Output: 1 2 3 4
# Exclusive range iteration
(1...4).each { |i| print "#{i} " }
# Output: 1 2 3
# Character range iteration
('a'..'d').each { |char| print "#{char} " }
# Output: a b c d
The step
method provides iteration with custom intervals, accepting a step value that determines the increment between successive elements. Step values must be positive for ascending ranges and negative for descending ranges. When the step value doesn't align with the range boundaries, iteration stops before reaching values that would exceed the endpoint.
# Forward stepping
(1..10).step(3) { |n| print "#{n} " }
# Output: 1 4 7 10
# Backward stepping requires reverse iteration
10.downto(1).step(2) { |n| print "#{n} " }
# Error: step requires positive value
# Correct backward iteration
(1..10).to_a.reverse.each_slice(2) { |pair| print "#{pair.first} " }
Time ranges support iteration with duration-based steps using step
method with time intervals. Ruby automatically handles time zone considerations and daylight saving transitions during iteration. The step value accepts numeric values representing seconds or Duration objects for more complex intervals.
# Time range with hourly steps
start_time = Time.parse("2024-01-01 00:00:00")
end_time = Time.parse("2024-01-01 06:00:00")
(start_time..end_time).step(3600) do |time|
puts time.strftime("%H:%M")
end
# Output: 00:00, 01:00, 02:00, 03:00, 04:00, 05:00, 06:00
# Date range iteration
require 'date'
(Date.today..Date.today + 5).each { |date| puts date }
Enumerable methods transform range iteration into collection operations. Methods like map
, select
, and reduce
convert ranges into arrays or computed values. The to_a
method materializes the entire range into an array, while methods like first
and take
provide partial materialization for memory efficiency.
# Range transformation with map
squares = (1..5).map { |n| n * n }
# => [1, 4, 9, 16, 25]
# Filtering with select
evens = (1..20).select(&:even?)
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# Aggregation with reduce
sum = (1..100).reduce(:+)
# => 5050
# Partial materialization
(1..1000).first(5)
# => [1, 2, 3, 4, 5]
Advanced Usage
Infinite ranges represent unbounded sequences using the ..
or ...
operators with nil
as the endpoint or the beginless/endless syntax. Infinite ranges require lazy evaluation to prevent infinite loops and memory exhaustion. The lazy
method creates an enumerator that evaluates elements on demand rather than materializing the entire sequence.
# Endless range with lazy evaluation
infinite_evens = (2..).lazy.select(&:even?)
infinite_evens.take(5).to_a
# => [2, 4, 6, 8, 10]
# Beginless range (Ruby 2.7+)
negative_numbers = (..0).to_a # Error: can't iterate from beginning
# Requires explicit bounds
negative_range = (-10..0).to_a
# => [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0]
# Chaining lazy operations
fibonacci_like = (1..).lazy.map { |n| n * (n + 1) / 2 }.take(8)
fibonacci_like.to_a
# => [1, 3, 6, 10, 15, 21, 28, 36]
Custom objects enable range iteration by implementing succ
, <=>
, and optionally pred
methods. The succ
method defines the next element in the sequence, while <=>
provides comparison for range boundary checking. Objects must maintain consistent ordering and successor relationships for proper iteration behavior.
class Version
attr_reader :major, :minor, :patch
def initialize(version_string)
@major, @minor, @patch = version_string.split('.').map(&:to_i)
end
def succ
Version.new("#{major}.#{minor}.#{patch + 1}")
end
def <=>(other)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
def to_s
"#{major}.#{minor}.#{patch}"
end
end
# Custom range iteration
v1 = Version.new("1.0.0")
v2 = Version.new("1.0.5")
(v1..v2).each { |version| puts version }
# Output: 1.0.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4, 1.0.5
Enumerator objects provide external iteration control for ranges, enabling manual advancement through sequences and integration with enumeration patterns. The to_enum
method creates enumerators from range iteration methods, allowing stateful iteration with next
and rewind
methods.
# External iterator for range
enum = (1..5).to_enum
puts enum.next # 1
puts enum.next # 2
enum.rewind
puts enum.next # 1
# Enumerator with custom step
step_enum = (0..20).step(5).to_enum
step_enum.with_index do |value, index|
puts "Position #{index}: #{value}"
end
# Output: Position 0: 0, Position 1: 5, Position 2: 10, Position 3: 15, Position 4: 20
Range iteration combines with pattern matching and destructuring for complex data processing. Multiple ranges can be zipped together for parallel iteration, and nested ranges create multidimensional iteration patterns. The combination enables matrix-like operations and coordinate-based algorithms.
# Parallel range iteration
x_coords = (0..2)
y_coords = (0..2)
coordinates = x_coords.to_a.product(y_coords.to_a)
# => [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]
# Nested range iteration for grid processing
(1..3).each do |row|
(1..3).each do |col|
print "[#{row},#{col}] "
end
puts
end
# Output: [1,1] [1,2] [1,3]
# [2,1] [2,2] [2,3]
# [3,1] [3,2] [3,3]
# Range-based matrix operations
matrix = (0...3).map { |i| (0...3).map { |j| i * 3 + j } }
# => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
Performance & Memory
Range iteration performance varies significantly based on range size, step intervals, and evaluation strategy. Lazy evaluation prevents memory allocation for large ranges, while eager evaluation through to_a
materializes entire sequences in memory. Understanding evaluation strategies prevents performance bottlenecks in range-heavy operations.
Small finite ranges perform efficiently with direct iteration, typically completing in microseconds for ranges under 1,000 elements. The iteration overhead remains minimal compared to array traversal due to Ruby's optimized range implementation. However, large ranges exceeding 100,000 elements show noticeable performance differences between lazy and eager evaluation.
# Performance comparison for large ranges
require 'benchmark'
Benchmark.bm do |x|
x.report("Eager evaluation:") { (1..100_000).to_a.select(&:even?) }
x.report("Lazy evaluation:") { (1..100_000).lazy.select(&:even?).first(1000) }
x.report("Step iteration:") { (2..100_000).step(2).first(1000) }
end
# Results show lazy evaluation performs better for partial results
# Eager: ~50ms, materializes full array
# Lazy: ~5ms, evaluates only needed elements
# Step: ~1ms, skips unnecessary elements entirely
Memory usage patterns differ dramatically between evaluation strategies. Eager evaluation allocates arrays proportional to range size, while lazy evaluation maintains constant memory usage regardless of range bounds. Infinite ranges require lazy evaluation to prevent memory exhaustion and program termination.
# Memory usage demonstration
def memory_usage
GC.stat[:total_allocated_objects]
end
before = memory_usage
# Eager evaluation allocates large array
large_array = (1..50_000).to_a
after_eager = memory_usage
# Lazy evaluation maintains minimal allocation
lazy_enum = (1..50_000).lazy
after_lazy = memory_usage
puts "Eager allocation: #{after_eager - before} objects"
puts "Lazy allocation: #{after_lazy - after_eager} objects"
# Eager: ~50,000 objects
# Lazy: <100 objects
Step-based iteration optimizes performance by reducing iteration cycles and memory allocation. Larger step values decrease iteration time proportionally, while smaller steps approach standard each
performance. The optimization applies particularly to mathematical sequences and sampling operations where full resolution is unnecessary.
# Step optimization for mathematical operations
# Calculate sum of every 100th number in range
sum_all = (1..10_000).reduce(:+) # ~2ms, 10,000 iterations
sum_step = (100..10_000).step(100).reduce(:+) # ~0.2ms, 100 iterations
# Performance scales with step size
(1..1_000_000).step(1000).each { |n| process(n) } # Fast
(1..1_000_000).step(1).each { |n| process(n) } # Slow
String and Time range iteration performance depends heavily on the complexity of successor calculation. String ranges with single-character increments perform efficiently, while complex string patterns require more computation per iteration. Time ranges perform well but involve timezone and calendar calculations that add overhead.
# String range performance comparison
Benchmark.bm do |x|
x.report("Single char:") { ('a'..'z').each { |c| c.upcase } }
x.report("Multi char:") { ('aa'..'zz').each { |s| s.upcase } }
end
# Single character ranges: ~0.001ms per iteration
# Multi character ranges: ~0.01ms per iteration
# Time range optimization
start_time = Time.now
end_time = start_time + 3600 # 1 hour later
# Efficient: step by minutes
(start_time..end_time).step(60).each { |time| log_time(time) }
# Inefficient: step by seconds
(start_time..end_time).step(1).each { |time| log_time(time) }
Common Pitfalls
Range boundary conditions create subtle bugs when mixing inclusive and exclusive ranges. Developers frequently confuse the ..
and ...
operators, leading to off-by-one errors in iteration limits. The difference becomes critical in array indexing and mathematical calculations where precise bounds matter.
# Boundary confusion with exclusive ranges
arr = [10, 20, 30, 40, 50]
# Incorrect: exclusive range misses last element
(0...arr.length).each { |i| puts arr[i] } # Correct, includes index 4
(0..arr.length).each { |i| puts arr[i] } # Error: index 5 doesn't exist
# Date range boundary issues
start_date = Date.parse("2024-01-01")
end_date = Date.parse("2024-01-05")
# Inclusive range: 5 days total
(start_date..end_date).count # => 5
# Exclusive range: 4 days total
(start_date...end_date).count # => 4
Infinite range iteration without lazy evaluation causes infinite loops and memory exhaustion. Ruby allows creation of infinite ranges but requires explicit lazy evaluation or limiting methods to prevent runaway execution. Missing the lazy
call results in program hangs and memory allocation errors.
# Dangerous: infinite loop without lazy evaluation
# (1..).each { |n| puts n } # Never terminates
# Safe: lazy evaluation with limits
(1..).lazy.take(10).each { |n| puts n } # Prints 1-10
# Common mistake: forgetting lazy with select
# (1..).select(&:even?) # Hangs trying to find all even numbers
# Correct approach
(1..).lazy.select(&:even?).take(5).to_a # => [2, 4, 6, 8, 10]
Step values create unexpected iteration patterns when they don't divide evenly into range spans. Developers expect step iteration to always reach the end value, but Ruby stops when the next step would exceed the range boundary. This behavior affects mathematical calculations and sequence generation.
# Step doesn't reach end value
(1..10).step(3).to_a # => [1, 4, 7, 10] - includes 10
(1...10).step(3).to_a # => [1, 4, 7] - excludes 10
# Unexpected step behavior with floats
(0.0..1.0).step(0.3).to_a # => [0.0, 0.3, 0.6, 0.9] - misses 1.0
(0.0..1.0).step(0.25).to_a # => [0.0, 0.25, 0.5, 0.75, 1.0] - includes 1.0
# Time step precision issues
start_time = Time.parse("12:00:00")
end_time = Time.parse("12:05:00")
(start_time..end_time).step(90).to_a.length # May not include end_time
Type coercion errors occur when range endpoints don't support successor methods or comparison operations. Ruby requires consistent types across range boundaries, and mixing incompatible types raises exceptions during iteration. The errors often surface during runtime rather than range creation.
# Type mismatch in range creation
begin
(1.."5").each { |n| puts n } # Error: can't compare Integer with String
rescue ArgumentError => e
puts e.message # bad value for range
end
# Mixed numeric types work but may surprise
(1..5.0).each { |n| puts "#{n} (#{n.class})" }
# Output: 1 (Integer), 2 (Integer), 3 (Integer), 4 (Integer), 5 (Integer)
# Custom objects without proper comparison
class BadRange
def initialize(value)
@value = value
end
end
# Missing methods cause iteration failure
# (BadRange.new(1)..BadRange.new(5)).each { |obj| puts obj }
# Error: can't iterate
Float range iteration produces precision errors due to floating-point arithmetic limitations. Small step values accumulate rounding errors, causing iteration to terminate early or skip expected values. The behavior varies across different Ruby versions and system architectures.
# Float precision problems
(0.0..1.0).step(0.1).to_a.length # Expected 11, may get 10
(0.0..1.0).step(0.1).last # May be 0.9999999999 instead of 1.0
# Safer approach using integer math
(0..10).step(1).map { |i| i / 10.0 } # => [0.0, 0.1, 0.2, ..., 1.0]
# Rational numbers avoid precision issues
require 'rational'
(0..10).step(1).map { |i| Rational(i, 10) } # Exact decimal representation
Reference
Core Range Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each { |obj| block } |
Block (required) | Range |
Iterates through range elements, yielding each to block |
#each |
None | Enumerator |
Returns enumerator for external iteration |
#step(n) { |obj| block } |
n (Numeric), Block |
Range |
Iterates with step interval, yielding every nth element |
#step(n) |
n (Numeric) |
Enumerator |
Returns stepped enumerator |
#to_a |
None | Array |
Materializes range into array (finite ranges only) |
#cover?(obj) |
obj (Object) |
Boolean |
Tests if object falls within range bounds |
#include?(obj) |
obj (Object) |
Boolean |
Tests if object exists in range sequence |
Enumerable Methods on Ranges
Method | Parameters | Returns | Description |
---|---|---|---|
#map { |obj| block } |
Block | Array |
Transforms each element, returns array of results |
#select { |obj| block } |
Block | Array |
Filters elements matching block condition |
#reject { |obj| block } |
Block | Array |
Filters elements not matching block condition |
#reduce(initial) { |acc, obj| block } |
Initial value, Block | Object |
Accumulates elements using block operation |
#first(n) |
n (Integer, optional) |
Object or Array |
Returns first element or first n elements |
#take(n) |
n (Integer) |
Array |
Returns first n elements as array |
#lazy |
None | Enumerator::Lazy |
Returns lazy enumerator for memory-efficient iteration |
Range Creation Syntax
Syntax | Type | Description | Example |
---|---|---|---|
start..end |
Inclusive | Includes both start and end values | (1..5) → [1, 2, 3, 4, 5] |
start...end |
Exclusive | Includes start, excludes end value | (1...5) → [1, 2, 3, 4] |
(start..) |
Endless | Infinite range starting from start | (1..) → [1, 2, 3, ...] |
(..end) |
Beginless | Range ending at end (Ruby 2.7+) | (..5) → [..., 3, 4, 5] |
(...end) |
Beginless Exclusive | Range ending before end | (...5) → [..., 2, 3, 4] |
Supported Range Types
Type | Successor Method | Step Support | Iteration Behavior |
---|---|---|---|
Integer |
Built-in | Yes | Increments by 1 or step value |
Float |
Built-in | Yes | May accumulate precision errors |
String |
Built-in | Limited | Single-character or pattern-based |
Time |
Built-in | Yes | Handles timezone and DST transitions |
Date |
Built-in | Yes | Calendar-aware progression |
Custom Objects | #succ required |
If implemented | Depends on #succ implementation |
Performance Characteristics
Operation | Time Complexity | Memory Usage | Best Practice |
---|---|---|---|
#each iteration |
O(n) | O(1) | Use for complete traversal |
#step iteration |
O(n/step) | O(1) | Optimize with larger steps |
#to_a materialization |
O(n) | O(n) | Avoid for large ranges |
Lazy evaluation | O(k) for k elements | O(1) | Use for partial results |
Infinite ranges | N/A without lazy | Infinite | Require #lazy or limits |
Common Error Patterns
Error Type | Cause | Solution |
---|---|---|
ArgumentError: bad value for range |
Type mismatch between start/end | Ensure consistent comparable types |
SystemStackError: stack level too deep |
Infinite iteration without lazy | Use #lazy with infinite ranges |
NoMethodError: undefined method 'succ' |
Custom object lacks successor | Implement #succ method |
Off-by-one errors | Inclusive vs exclusive confusion | Double-check .. vs ... usage |
Precision errors | Float step accumulation | Use integer math or Rational |
Iteration Control Methods
Method | Purpose | Example Usage |
---|---|---|
break |
Exit iteration early | (1..100).each { |n| break if n > 10 } |
next |
Skip current iteration | (1..10).each { |n| next if n.even? } |
redo |
Repeat current iteration | Complex state-dependent processing |
return |
Exit containing method | Early return from iteration context |