Overview
Ruby's Range#step method provides iteration through a range with specified increments, extending beyond built-in numeric types to work with any objects that implement the required protocol methods. Custom objects can participate in range stepping by defining specific methods that Range#step relies on for arithmetic operations and comparisons.
The step protocol requires custom objects to implement comparison operators (<=>
, <
, >
, <=
, >=
), addition (+
), and subtraction (-
) methods. Range#step uses these methods to calculate iteration boundaries, perform increment operations, and determine when to terminate iteration.
class Temperature
attr_reader :celsius
def initialize(celsius)
@celsius = celsius
end
def +(other)
Temperature.new(celsius + other.celsius)
end
def <=>(other)
celsius <=> other.celsius
end
def to_s
"#{celsius}°C"
end
end
start_temp = Temperature.new(20)
end_temp = Temperature.new(30)
step_temp = Temperature.new(2)
(start_temp..end_temp).step(step_temp) do |temp|
puts temp
end
# 20°C
# 22°C
# 24°C
# 26°C
# 28°C
# 30°C
Ruby's Range implementation calls the +
method on the current value with the step value as the argument during each iteration. The comparison operators determine iteration boundaries and termination conditions. Objects that lack these methods raise NoMethodError
when used with Range#step.
Basic Usage
Range#step with custom objects requires implementing the comparison and arithmetic protocol. The comparison operator <=>
establishes ordering relationships, while addition defines how stepping occurs. Ruby calls these methods during iteration to advance from the starting value toward the ending value.
class Version
attr_reader :major, :minor, :patch
def initialize(major, minor = 0, patch = 0)
@major, @minor, @patch = major, minor, patch
end
def +(step)
Version.new(major, minor, patch + step.patch)
end
def <=>(other)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
def to_s
"#{major}.#{minor}.#{patch}"
end
end
start_version = Version.new(1, 0, 0)
end_version = Version.new(1, 0, 5)
step_increment = Version.new(0, 0, 1)
versions = []
(start_version..end_version).step(step_increment) do |version|
versions << version.to_s
end
versions
# => ["1.0.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5"]
The step method can work with any increment value that responds to the required protocol. Ruby does not enforce that step values match the range element type, allowing mixed-type operations when objects define appropriate methods.
class Counter
attr_reader :value
def initialize(value)
@value = value
end
def +(increment)
case increment
when Integer
Counter.new(value + increment)
when Counter
Counter.new(value + increment.value)
end
end
def <=>(other)
value <=> other.value
end
def to_s
"Count: #{value}"
end
end
start_counter = Counter.new(1)
end_counter = Counter.new(10)
# Using integer step
(start_counter..end_counter).step(3) do |counter|
puts counter
end
# Count: 1
# Count: 4
# Count: 7
# Count: 10
Range#step supports both inclusive (..
) and exclusive (...
) ranges with custom objects. Exclusive ranges terminate iteration before reaching the end value, while inclusive ranges include the end value when step calculations land exactly on it.
class Letter
attr_reader :char
def initialize(char)
@char = char
end
def +(step)
Letter.new((char.ord + step).chr)
end
def <=>(other)
char <=> other.char
end
def to_s
char
end
end
start_letter = Letter.new('a')
end_letter = Letter.new('f')
# Inclusive range
inclusive_result = []
(start_letter..end_letter).step(2) { |letter| inclusive_result << letter.to_s }
inclusive_result
# => ["a", "c", "e"]
# Exclusive range
exclusive_result = []
(start_letter...end_letter).step(2) { |letter| exclusive_result << letter.to_s }
exclusive_result
# => ["a", "c", "e"]
Advanced Usage
Range#step with custom objects supports complex stepping patterns through sophisticated protocol implementations. Objects can define context-sensitive arithmetic operations, implement multiple step strategies, and handle edge cases in comparison and iteration logic.
class Coordinate
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def +(step)
case step
when Coordinate
Coordinate.new(x + step.x, y + step.y)
when Hash
Coordinate.new(x + step.fetch(:x, 0), y + step.fetch(:y, 0))
else
Coordinate.new(x + step, y + step)
end
end
def <=>(other)
# Compare by distance from origin
distance_self = Math.sqrt(x**2 + y**2)
distance_other = Math.sqrt(other.x**2 + other.y**2)
distance_self <=> distance_other
end
def to_s
"(#{x}, #{y})"
end
end
start_coord = Coordinate.new(0, 0)
end_coord = Coordinate.new(5, 5)
# Step with coordinate object
coord_step = Coordinate.new(1, 2)
coord_results = []
(start_coord..end_coord).step(coord_step) do |coord|
coord_results << coord.to_s
end
coord_results
# => ["(0, 0)", "(1, 2)", "(2, 4)"]
# Step with hash parameter
hash_results = []
(start_coord..end_coord).step(x: 2, y: 1) do |coord|
hash_results << coord.to_s
end
hash_results
# => ["(0, 0)", "(2, 1)", "(4, 2)"]
Custom objects can implement complex stepping logic that handles multiple dimensions, state transitions, or domain-specific increment patterns. The protocol allows objects to interpret step values according to their internal semantics.
class StateMachine
STATES = [:idle, :processing, :complete, :error]
attr_reader :state, :iteration
def initialize(state, iteration = 0)
@state = state
@iteration = iteration
end
def +(step)
case step
when Integer
new_iteration = iteration + step
StateMachine.new(state, new_iteration)
when Symbol
state_index = STATES.index(state)
step_index = STATES.index(step)
return self unless step_index
new_state_index = (state_index + step_index) % STATES.length
StateMachine.new(STATES[new_state_index], iteration + 1)
when StateMachine
state_index = STATES.index(state)
step_state_index = STATES.index(step.state)
new_state_index = (state_index + step_state_index + 1) % STATES.length
StateMachine.new(STATES[new_state_index], iteration + step.iteration)
end
end
def <=>(other)
[STATES.index(state), iteration] <=> [STATES.index(other.state), other.iteration]
end
def to_s
"#{state}(#{iteration})"
end
end
start_state = StateMachine.new(:idle, 0)
end_state = StateMachine.new(:complete, 5)
# Step by state transition
state_step = StateMachine.new(:processing, 1)
state_results = []
(start_state..end_state).step(state_step) do |state|
state_results << state.to_s
end
state_results
# => ["idle(0)", "error(1)", "processing(2)"]
Range#step can work with objects that implement lazy evaluation or caching strategies within their arithmetic methods. This enables memory-efficient iteration over large conceptual ranges or computationally expensive step calculations.
class LazySequence
attr_reader :base, :multiplier
def initialize(base, multiplier = 2)
@base = base
@multiplier = multiplier
@cache = {}
end
def +(step)
case step
when LazySequence
new_multiplier = (multiplier + step.multiplier) / 2.0
LazySequence.new(calculate_next(step.base), new_multiplier)
when Numeric
LazySequence.new(calculate_next(step), multiplier)
end
end
def <=>(other)
base <=> other.base
end
def to_s
"Seq(#{base})"
end
private
def calculate_next(step_value)
cache_key = [base, step_value, multiplier]
@cache[cache_key] ||= (base + step_value) * multiplier
end
end
start_seq = LazySequence.new(1, 1.5)
end_seq = LazySequence.new(20, 1.5)
step_seq = LazySequence.new(2, 1.2)
lazy_results = []
(start_seq..end_seq).step(step_seq) do |seq|
lazy_results << seq.to_s
break if lazy_results.size >= 3 # Limit for demonstration
end
lazy_results
# => ["Seq(1)", "Seq(4.5)", "Seq(12.15)"]
Common Pitfalls
Range#step with custom objects introduces several subtle issues around method implementation, comparison consistency, and arithmetic operations. Objects that implement comparison operators inconsistently or arithmetic methods that violate mathematical properties cause iteration failures or infinite loops.
The most frequent issue involves implementing <=>
without ensuring transitivity and consistency. Ruby's Range#step relies on comparison operations to determine iteration termination, and inconsistent comparisons lead to unpredictable behavior.
class InconsistentComparator
attr_reader :value
def initialize(value)
@value = value
end
def +(step)
InconsistentComparator.new(value + step.value)
end
def <=>(other)
# Inconsistent: comparison result varies based on internal state
rand(3) - 1 # Returns -1, 0, or 1 randomly
end
def to_s
value.to_s
end
end
start_inconsistent = InconsistentComparator.new(1)
end_inconsistent = InconsistentComparator.new(5)
step_inconsistent = InconsistentComparator.new(1)
# This may not terminate or may skip values unpredictably
begin
results = []
(start_inconsistent..end_inconsistent).step(step_inconsistent) do |obj|
results << obj.to_s
break if results.size > 10 # Safety break
end
rescue => e
puts "Error during iteration: #{e.message}"
end
Another common pitfall occurs when arithmetic operations do not preserve the mathematical properties expected by Range#step. Operations that are not associative or do not maintain ordering relationships cause step calculations to produce unexpected results.
class NonAssociativeAdder
attr_reader :value
def initialize(value)
@value = value
end
def +(step)
# Non-associative: result depends on order of operations
new_value = (value + step.value) % 7 # Modulo breaks expected ordering
NonAssociativeAdder.new(new_value)
end
def <=>(other)
value <=> other.value
end
def to_s
value.to_s
end
end
start_mod = NonAssociativeAdder.new(1)
end_mod = NonAssociativeAdder.new(6)
step_mod = NonAssociativeAdder.new(2)
mod_results = []
(start_mod..end_mod).step(step_mod) do |obj|
mod_results << obj.to_s
break if mod_results.size > 8 # Prevent infinite loops
end
mod_results
# May produce: ["1", "3", "5", "0", "2", "4", "6"] - wrapping unexpectedly
Range#step assumes that step operations move monotonically toward the end value. Objects that implement addition in ways that can move backward or sideways relative to the comparison ordering create iteration issues.
class BackwardStepper
attr_reader :primary, :secondary
def initialize(primary, secondary = 0)
@primary = primary
@secondary = secondary
end
def +(step)
if secondary > primary
# Moves backward in primary dimension when secondary is large
BackwardStepper.new(primary - step.primary, secondary + step.secondary)
else
BackwardStepper.new(primary + step.primary, secondary + step.secondary)
end
end
def <=>(other)
primary <=> other.primary
end
def to_s
"#{primary}:#{secondary}"
end
end
start_backward = BackwardStepper.new(1, 0)
end_backward = BackwardStepper.new(5, 0)
step_backward = BackwardStepper.new(1, 2)
backward_results = []
(start_backward..end_backward).step(step_backward) do |obj|
backward_results << obj.to_s
break if backward_results.size > 6
end
backward_results
# May produce unexpected results due to backward movement
Type coercion issues arise when custom objects interact with built-in Ruby types during step operations. Objects that do not handle mixed-type arithmetic gracefully raise runtime exceptions during iteration.
class StrictTyper
attr_reader :value
def initialize(value)
@value = value
end
def +(step)
unless step.is_a?(StrictTyper)
raise TypeError, "Cannot add #{step.class} to StrictTyper"
end
StrictTyper.new(value + step.value)
end
def <=>(other)
value <=> other.value
end
def to_s
value.to_s
end
end
start_strict = StrictTyper.new(1)
end_strict = StrictTyper.new(10)
begin
# This will raise TypeError because step is Integer, not StrictTyper
(start_strict..end_strict).step(2) do |obj|
puts obj
end
rescue TypeError => e
puts "Type error: #{e.message}"
end
# Type error: Cannot add Integer to StrictTyper
# Correct usage with matching types
correct_step = StrictTyper.new(2)
strict_results = []
(start_strict..end_strict).step(correct_step) do |obj|
strict_results << obj.to_s
end
strict_results
# => ["1", "3", "5", "7", "9"]
Reference
Required Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#+(other) |
other (Object) |
Same class instance | Adds step value to current object |
#<=>(other) |
other (Object) |
-1 , 0 , 1 , or nil |
Comparison for ordering and equality |
#<(other) |
other (Object) |
Boolean |
Less than comparison (optional, derived from <=> ) |
#>(other) |
other (Object) |
Boolean |
Greater than comparison (optional, derived from <=> ) |
#<=(other) |
other (Object) |
Boolean |
Less than or equal comparison (optional, derived from <=> ) |
#>=(other) |
other (Object) |
Boolean |
Greater than or equal comparison (optional, derived from <=> ) |
Range#step Behavior
Range Type | Step Direction | Iteration Boundary |
---|---|---|
(start..end) |
Positive | Includes end value if reached exactly |
(start...end) |
Positive | Excludes end value |
(start..end) |
Zero | Raises ArgumentError |
(start..end) |
Negative | No iteration when start < end |
Implementation Patterns
# Basic protocol implementation
def +(step)
self.class.new(value + step.value)
end
def <=>(other)
value <=> other.value
end
# Type-flexible addition
def +(step)
case step
when Integer
self.class.new(value + step)
when self.class
self.class.new(value + step.value)
else
raise TypeError, "Cannot add #{step.class}"
end
end
# Multi-attribute comparison
def <=>(other)
[primary_attr, secondary_attr] <=> [other.primary_attr, other.secondary_attr]
end
Common Error Conditions
Error Type | Cause | Prevention |
---|---|---|
NoMethodError |
Missing + or <=> methods |
Implement required protocol methods |
ArgumentError |
Zero step value | Validate step value in calling code |
TypeError |
Incompatible step type | Handle type coercion in + method |
Infinite loop | Non-monotonic step operation | Ensure + moves toward end value |
Inconsistent results | Non-transitive comparison | Implement consistent <=> logic |
Protocol Validation
# Check if object supports Range#step protocol
def step_compatible?(obj)
obj.respond_to?(:+) && obj.respond_to?(:<=>)
end
# Validate step behavior
def valid_step_operation?(start_obj, step_obj, end_obj)
return false unless step_compatible?(start_obj)
next_obj = start_obj + step_obj
return false unless next_obj.class == start_obj.class
# Check monotonic progress
case start_obj <=> end_obj
when -1 # start < end
(next_obj <=> start_obj) == 1
when 1 # start > end
(next_obj <=> start_obj) == -1
else
false
end
end
Performance Considerations
Pattern | Memory Usage | CPU Usage | Scalability |
---|---|---|---|
Simple value objects | Low | Low | High |
Cached calculations | Medium | Medium | High |
Complex state objects | High | High | Medium |
Lazy evaluation | Low | Variable | High |