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 |