CrackedRuby logo

CrackedRuby

Range step with Custom Objects

Documentation for using Range#step with custom objects that implement the necessary protocol methods for range iteration.

Core Built-in Classes Range Class
2.6.6

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