CrackedRuby logo

CrackedRuby

Range Iteration

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