CrackedRuby logo

CrackedRuby

Endless and Beginless Ranges

Ruby ranges with open boundaries for pattern matching, array slicing, and conditional logic.

Core Built-in Classes Range Class
2.6.5

Overview

Ruby supports ranges with open boundaries through endless and beginless range syntax. Endless ranges start at a specific value but have no upper bound, written as (start..) for inclusive or (start...) for exclusive endings. Beginless ranges have no lower bound but end at a specific value, written as (..end) for inclusive or (...end) for exclusive beginnings.

These ranges extend Ruby's standard Range class with nil values representing the missing boundaries. The range (1..) creates a Range object where #begin returns 1 and #end returns nil. Similarly, (..10) creates a range where #begin returns nil and #end returns 10.

Endless and beginless ranges integrate with Ruby's existing range operations while introducing specific behaviors for boundary handling. They work with pattern matching, case statements, array indexing, and enumerable operations, though iteration over endless ranges requires explicit limits.

# Endless range creation
numbers = (1..)
numbers.begin    # => 1
numbers.end      # => nil
numbers.size     # => Infinity

# Beginless range creation  
negative_numbers = (..0)
negative_numbers.begin   # => nil
negative_numbers.end     # => 0

The primary use cases include array slicing with dynamic bounds, pattern matching against open-ended conditions, implementing pagination logic, and creating flexible comparison operations in case statements.

Basic Usage

Array Slicing Operations

Endless and beginless ranges excel at array slicing when the boundary position depends on runtime conditions or when extracting variable-length subsequences.

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Extract from index 3 to end
tail = data[3..]              # => [4, 5, 6, 7, 8, 9, 10]

# Extract from start to index 4
head = data[..4]              # => [1, 2, 3, 4, 5]

# Extract from start up to but not including index 4
head_exclusive = data[...4]   # => [1, 2, 3, 4]

String slicing follows identical patterns, making substring extraction more readable when dealing with variable positions.

text = "Hello, World!"

# Get everything after position 7
suffix = text[7..]     # => "World!"

# Get everything up to position 5
prefix = text[..5]     # => "Hello,"

# Extract middle portion with mixed bounds
middle = text[2..-2]   # => "llo, Worl"

Pattern Matching Integration

Endless and beginless ranges work within pattern matching expressions to handle open-ended conditions.

def categorize_age(age)
  case age
  in (..12)
    "child"
  in (13..17)
    "teenager"  
  in (18..64)
    "adult"
  in (65..)
    "senior"
  end
end

categorize_age(8)    # => "child"
categorize_age(25)   # => "adult"  
categorize_age(70)   # => "senior"

Membership Testing

Range membership testing handles boundary conditions automatically, making conditional logic more expressive.

def validate_score(score)
  case score
  when (..0)
    "Score cannot be negative"
  when (0..100)
    "Valid score: #{score}"
  when (101..)
    "Score cannot exceed 100"
  end
end

validate_score(-5)    # => "Score cannot be negative"
validate_score(42)    # => "Valid score: 42"
validate_score(150)   # => "Score cannot exceed 100"

Enumerable Operations

Beginless ranges support enumerable operations when they have a defined starting point through intersection with bounded ranges.

# Find numbers in beginless range that match criteria
valid_numbers = (..100).select { |n| n > 95 }
# This creates an infinite loop - see Common Pitfalls section

# Safe approach using intersection
bounded_check = (90..100).select { |n| (..98).include?(n) }
# => [90, 91, 92, 93, 94, 95, 96, 97, 98]

Advanced Usage

Complex Array Manipulation

Endless and beginless ranges enable sophisticated array manipulation patterns, especially when combined with multiple assignment and conditional slicing.

def partition_array(array, threshold_index)
  return [[], array] if threshold_index <= 0
  return [array, []] if threshold_index >= array.length
  
  [array[..threshold_index-1], array[threshold_index..]]
end

data = %w[a b c d e f g h i j]
left, right = partition_array(data, 4)
# left => ["a", "b", "c", "d"]
# right => ["e", "f", "g", "h", "i", "j"]

# Dynamic slicing based on content
def extract_after_marker(array, marker)
  marker_index = array.index(marker)
  return [] unless marker_index
  
  array[(marker_index + 1)..]
end

log_entries = ["INFO", "DEBUG", "ERROR", "WARN", "FATAL"]
errors_and_beyond = extract_after_marker(log_entries, "ERROR")
# => ["WARN", "FATAL"]

Range Arithmetic and Set Operations

Ranges support intersection and set-like operations, though endless and beginless ranges require special handling.

def safe_range_intersection(range1, range2)
  # Handle endless/beginless range intersections
  start_val = [range1.begin, range2.begin].compact.max
  end_val = [range1.end, range2.end].compact.min
  
  return nil if start_val && end_val && start_val > end_val
  
  case [range1.exclude_end?, range2.exclude_end?]
  when [true, true], [true, false], [false, true]
    (start_val...end_val)
  else
    (start_val..end_val)
  end
end

range_a = (10..)
range_b = (..50)
intersection = safe_range_intersection(range_a, range_b)
# => (10..50)

range_c = (100..)
range_d = (..50)  
no_intersection = safe_range_intersection(range_c, range_d)
# => nil

Method Parameter Patterns

Endless and beginless ranges work effectively as method parameters for implementing flexible APIs with default boundary behavior.

class DataProcessor
  def process_records(records, range: (0..))
    selected_records = case range
                      when Range
                        records.values_at(*range.to_a.select { |i| i < records.length })
                      else
                        records
                      end
    
    selected_records.compact
  end
  
  def extract_window(data, start_range: (0..), end_range: (..data.length-1))
    start_indices = start_range.is_a?(Range) ? [start_range.begin || 0] : [start_range]
    end_indices = end_range.is_a?(Range) ? [end_range.end || data.length-1] : [end_range]
    
    data[start_indices.first..end_indices.first]
  end
end

processor = DataProcessor.new
data = (1..20).to_a

# Process from index 5 onwards
result1 = processor.process_records(data, range: (5..))

# Extract window with beginless end range
result2 = processor.extract_window(data, start_range: 3, end_range: (..10))

Custom Range Classes

Building custom range-like classes that incorporate endless and beginless behavior requires implementing comparison and boundary methods.

class FlexibleDateRange
  include Enumerable
  
  attr_reader :start_date, :end_date
  
  def initialize(start_date, end_date)
    @start_date = start_date
    @end_date = end_date
  end
  
  def self.endless(start_date)
    new(start_date, nil)
  end
  
  def self.beginless(end_date)
    new(nil, end_date)
  end
  
  def include?(date)
    return false unless date.is_a?(Date)
    
    start_ok = start_date.nil? || date >= start_date
    end_ok = end_date.nil? || date <= end_date
    
    start_ok && end_ok
  end
  
  def each
    return enum_for(:each) unless block_given?
    
    raise ArgumentError, "Cannot enumerate endless range" if end_date.nil?
    
    current = start_date || Date.new(1900, 1, 1)
    while current <= end_date
      yield current
      current = current.next_day
    end
  end
end

# Usage examples
past_month = FlexibleDateRange.beginless(Date.today)
future_events = FlexibleDateRange.endless(Date.today + 30)

past_month.include?(Date.today - 10)  # => true
future_events.include?(Date.today + 60)  # => true

Common Pitfalls

Infinite Iteration Attempts

The most frequent mistake involves attempting to iterate over endless ranges without explicit boundaries, causing infinite loops or memory exhaustion.

# DANGEROUS: Infinite loop
endless_range = (1..)
# endless_range.each { |n| puts n }  # Never terminates

# SAFE: Use take, first, or lazy evaluation
endless_range.lazy.select(&:even?).first(5)  # => [2, 4, 6, 8, 10]
endless_range.first(10)  # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# SAFE: Combine with finite operations
result = (1..).take_while { |n| n < 100 }.select(&:odd?)
# => [1, 3, 5, 7, 9, ..., 97, 99]

Range conversion methods like #to_a fail catastrophically with endless ranges, while #size returns Float::INFINITY which may not integrate properly with other operations.

endless = (1..)
endless.size.class    # => Float
endless.size == Float::INFINITY  # => true

# WRONG: Memory exhaustion
# endless.to_a

# WRONG: Unexpected behavior in calculations  
array_length = [1, 2, 3].length
combined_size = array_length + endless.size  # => Infinity (probably not intended)

Beginless Range Enumeration

Beginless ranges cannot enumerate without an explicit starting point, but this limitation manifests in subtle ways during method chaining.

beginless = (..10)

# WRONG: No starting point for enumeration
# beginless.each { |n| puts n }  # Raises RangeError

# WRONG: Select without bounds
# beginless.select(&:even?)  # RangeError

# CORRECT: Test membership instead
[5, 15, 8, 2].select { |n| beginless.include?(n) }  # => [5, 8, 2]

# CORRECT: Use with intersection
(0..15).select { |n| beginless.include?(n) }  # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Boundary Inclusion Confusion

The distinction between inclusive (..) and exclusive (...) endings becomes critical with endless and beginless ranges, especially in edge cases.

# Inclusive vs exclusive with beginless ranges
data = [10, 20, 30, 40, 50]

# Inclusive beginless: includes the boundary value
inclusive_result = data.select { |n| (..30).include?(n) }  # => [10, 20, 30]

# Exclusive beginless: excludes the boundary value  
exclusive_result = data.select { |n| (...30).include?(n) }  # => [10, 20]

# Array slicing behaves differently
arr = %w[a b c d e]
arr[..2]   # => ["a", "b", "c"] (includes index 2)
arr[...2]  # => ["a", "b"] (excludes index 2)

# Endless ranges with exclusion
(5..).include?(5)    # => true
(5...).include?(5)   # => false (excludes start value)

Type Coercion Issues

Ruby's automatic type coercion can produce unexpected results when endless or beginless ranges interact with incompatible types.

# String ranges with missing boundaries
string_endless = ("a"..)
string_endless.include?("zebra")  # => true
string_endless.include?(100)      # => false, but no error

# Numeric comparison edge cases  
float_beginless = (..5.5)
float_beginless.include?(5)       # => true
float_beginless.include?("5")     # => ArgumentError in some contexts

# Date/Time ranges
require 'date'
date_endless = (Date.today..)
date_endless.include?(Time.now)   # => ArgumentError: comparison of Time with Date failed

# SAFE: Explicit type checking
def safe_range_include?(range, value)
  return false unless value.respond_to?(:<=>)
  
  begin
    range.include?(value)
  rescue ArgumentError
    false
  end
end

Memory Consumption with Large Finite Ranges

When endless or beginless ranges get bounded through operations, the resulting finite ranges might consume unexpected memory.

# Appears innocent but creates large array
bounded_endless = (1_000_000..).first(500_000)  # Creates 500k element array

# Better: Use lazy evaluation
lazy_bounded = (1_000_000..).lazy.first(500_000)
memory_friendly = lazy_bounded.select(&:even?).first(100)  # Only processes what's needed

# Beginless ranges combined with large finite ranges
huge_intersection = (1..10_000_000).select { |n| (..5_000_000).include?(n) }
# This creates a 5 million element array

# Better: Direct range creation
efficient_range = (1..5_000_000)  # Same result, no intermediate array

Reference

Range Creation Syntax

Syntax Type Description Example
(start..) Endless inclusive From start to infinity (5..)
(start...) Endless exclusive From after start to infinity (5...)
(..end) Beginless inclusive From negative infinity to end (..10)
(...end) Beginless exclusive From negative infinity before end (...10)

Core Methods

Method Parameters Returns Description
#begin None Object or nil Start value or nil for beginless
#end None Object or nil End value or nil for endless
#exclude_end? None Boolean Whether end value is excluded
#exclude_begin? None Boolean Whether begin value is excluded
#include?(obj) obj (Object) Boolean Tests membership in range
#cover?(obj) obj (Object) Boolean Tests if range covers value
#size None Integer or Float Range size, Infinity for endless
#to_a None Array Converts to array (fails for endless)
#first(n=1) n (Integer) Array First n elements
#last(n=1) n (Integer) Array Last n elements (fails for endless)

Enumeration Methods

Method Endless Support Beginless Support Notes
#each With limit only No Requires finite bounds
#select With limit only No Use with intersection
#map With limit only No Combine with first or take
#take(n) Yes No Safe for endless ranges
#take_while Yes No Stops at condition
#lazy Yes No Creates lazy enumerator

Array Indexing Operations

Operation Endless Beginless Example
array[range] Extracts to end Extracts from start arr[3..], arr[..5]
array[range] = value Replaces to end Replaces from start arr[3..] = [7, 8, 9]
array.slice(range) Same as [] Same as [] arr.slice(2..)

Comparison and Set Operations

Operation Result Type Notes
range == other Boolean Compares boundaries and exclusion
range.cover?(other_range) Boolean Tests if range covers another
range & other Not supported Use custom intersection method
range.overlap?(other) Boolean Ruby 2.6+ method

Common Patterns

# Safe endless iteration
(1..).lazy.select(&:even?).first(10)

# Beginless range testing  
numbers.select { |n| (..threshold).include?(n) }

# Array tail extraction
array[(array.length - 5)..]

# Pattern matching with ranges
case value
in (..0) then "negative"
in (1..) then "positive"
end

# Safe range intersection
def intersect(r1, r2)
  start_val = [r1.begin, r2.begin].compact.max
  end_val = [r1.end, r2.end].compact.min
  return nil if start_val && end_val && start_val > end_val
  (start_val..end_val)
end

Error Conditions

Error Cause Solution
RangeError Enumerating beginless range Use membership testing
SystemStackError Infinite iteration Use lazy, take, or first
NoMemoryError Converting endless to array Avoid to_a on endless ranges
ArgumentError Type mismatch in comparison Check types before range operations