Overview
Range Literals represent intervals between two values in Ruby. Ruby implements ranges through the Range class, which provides methods for creating, manipulating, and iterating over sequences of values. Range literals use the ..
and ...
operators to define inclusive and exclusive ranges respectively.
The Range class supports any objects that implement the <=>
spaceship operator and succ
method, making ranges work with integers, strings, dates, and custom objects. Ruby creates Range objects using literal syntax, with the start and end values determining the sequence boundaries.
# Inclusive range - includes both endpoints
inclusive_range = 1..10
# => 1..10
# Exclusive range - excludes the end value
exclusive_range = 1...10
# => 1...10
# String ranges work with lexicographical ordering
letter_range = 'a'..'z'
# => "a".."z"
Ruby ranges serve multiple purposes: iteration control, array slicing, pattern matching in case statements, and membership testing. The Range class provides both enumeration capabilities and mathematical interval operations.
Basic Usage
Range literals create Range objects using two dot operators. The inclusive operator ..
includes both start and end values, while the exclusive operator ...
excludes the end value. This distinction affects iteration, size calculation, and membership testing.
# Basic integer ranges
numbers = 1..5
numbers.to_a
# => [1, 2, 3, 4, 5]
exclusive_numbers = 1...5
exclusive_numbers.to_a
# => [1, 2, 3, 4]
# Testing membership
numbers.include?(5) # => true
exclusive_numbers.include?(5) # => false
String ranges follow lexicographical ordering, generating sequences based on ASCII values and string length. Single character ranges work predictably, while multi-character ranges can produce unexpected results.
# Character ranges
alphabet = 'a'..'e'
alphabet.to_a
# => ["a", "b", "c", "d", "e"]
# Multi-character string ranges
word_range = 'aa'..'ac'
word_range.to_a
# => ["aa", "ab", "ac"]
# Date ranges require explicit conversion
require 'date'
date_range = Date.new(2023, 1, 1)..Date.new(2023, 1, 7)
date_range.count
# => 7
Array slicing with ranges provides substring and subarray operations. Negative indices work with ranges, counting from the array end. The []
operator accepts ranges for extracting elements.
# Array slicing with ranges
array = [10, 20, 30, 40, 50, 60, 70]
array[1..3] # => [20, 30, 40]
array[1...3] # => [20, 30]
array[-3..-1] # => [50, 60, 70]
# String slicing
text = "programming"
text[0..4] # => "progr"
text[2...6] # => "ogra"
text[-4..-1] # => "ming"
Case statements accept ranges for pattern matching, testing whether a value falls within the range bounds. This creates readable conditional logic for numeric and comparable value testing.
def categorize_score(score)
case score
when 90..100
"Excellent"
when 80...90
"Good"
when 70...80
"Average"
when 0...70
"Needs Improvement"
else
"Invalid Score"
end
end
categorize_score(85) # => "Good"
categorize_score(90) # => "Excellent"
Advanced Usage
Infinite ranges provide unbounded intervals for advanced filtering and iteration patterns. Ruby supports beginless and endless ranges using nil
or omitting the start/end values. These ranges work with enumerable methods and comparison operations.
# Endless ranges (Ruby 2.6+)
endless = 1..
endless.include?(1_000_000) # => true
# Beginless ranges (Ruby 2.7+)
beginless = ..10
beginless.include?(-5) # => true
# Combined with array operations
numbers = [1, 5, 10, 15, 20, 25, 30]
numbers.select { |n| (10..).include?(n) }
# => [10, 15, 20, 25, 30]
filtered = numbers.select { |n| (..15).include?(n) }
# => [1, 5, 10, 15]
Custom objects work with ranges when they implement comparable methods. The <=>
spaceship operator enables range creation, while succ
supports enumeration. Range operations depend on proper implementation of these methods.
class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(version_string)
@major, @minor, @patch = version_string.split('.').map(&:to_i)
end
def <=>(other)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
def succ
Version.new("#{major}.#{minor}.#{patch + 1}")
end
def to_s
"#{major}.#{minor}.#{patch}"
end
end
v1 = Version.new("1.0.0")
v2 = Version.new("1.0.5")
version_range = v1..v2
# Range operations work with custom comparable objects
version_range.include?(Version.new("1.0.3")) # => true
version_range.to_a.map(&:to_s)
# => ["1.0.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5"]
Range stepping provides controlled iteration with custom increments. The step
method accepts block parameters for processing elements at specified intervals. This enables efficient processing of large ranges without creating intermediate arrays.
# Stepping through ranges with custom increments
(0..100).step(10) do |value|
puts "Processing batch #{value}"
end
# Outputs: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
# Creating arrays with step values
stepped_values = (1..20).step(3).to_a
# => [1, 4, 7, 10, 13, 16, 19]
# Date stepping for time-based iterations
require 'date'
start_date = Date.new(2023, 1, 1)
end_date = Date.new(2023, 1, 31)
weekly_dates = (start_date..end_date).step(7).to_a
# => [2023-01-01, 2023-01-08, 2023-01-15, 2023-01-22, 2023-01-29]
Range flipping and reversal operations provide alternative iteration orders. The reverse_each
method processes range elements in descending order. Range bounds can be swapped programmatically for dynamic range construction.
# Reverse iteration without creating intermediate arrays
countdown = []
(1..5).reverse_each { |i| countdown << i }
countdown # => [5, 4, 3, 2, 1]
# Dynamic range construction based on conditions
def create_range(start, stop, ascending: true)
if ascending
start..stop
else
stop..start
end
end
ascending_range = create_range(1, 10, ascending: true)
descending_range = create_range(10, 1, ascending: false)
# Both ranges work for membership testing
ascending_range.include?(5) # => true
descending_range.include?(5) # => true
Common Pitfalls
The inclusive versus exclusive range operators create frequent confusion, especially when working with array indices and boundary conditions. The ..
operator includes the end value, while ...
excludes it. This difference affects iteration counts, membership testing, and slicing operations.
# Array index confusion with exclusive ranges
array = ['a', 'b', 'c', 'd', 'e']
# These produce different results
array[1..3] # => ["b", "c", "d"] - includes index 3
array[1...3] # => ["b", "c"] - excludes index 3
# Length-based slicing pitfall
length = array.length # => 5
array[0..length] # => ["a", "b", "c", "d", "e", nil] - includes extra element
array[0...length] # => ["a", "b", "c", "d", "e"] - correct approach
# Range size differences
(1..5).size # => 5
(1...5).size # => 4
String range enumeration produces unexpected results with multi-character strings. Ruby generates string sequences by incrementing the rightmost character, then carrying over to create longer strings. This behavior often contradicts developer expectations.
# Unexpected string range behavior
('a'..'z').to_a.size # => 26 (expected)
('aa'..'az').to_a.size # => 26 (expected)
('aa'..'ba').to_a.size # => 27 (unexpected - includes 'az', 'ba')
# Multi-character ranges can create large sequences
large_range = 'aa'..'bb'
large_range.to_a.size # => 702 elements!
# String ranges with different lengths
mixed_range = 'a'..'aa'
mixed_range.to_a
# => ["a", "b", "c", ..., "z", "aa"]
# Comparing lengths affects range behavior
('z'..'aa').to_a.empty? # => true (invalid range)
Performance issues emerge when converting large ranges to arrays or using enumeration methods that materialize all values. Memory consumption grows linearly with range size, potentially causing memory exhaustion with large intervals.
# Memory-intensive range operations
large_range = 1..1_000_000
# These operations consume significant memory
large_array = large_range.to_a # Creates 1M element array
mapped_values = large_range.map(&:to_s) # Creates 1M string objects
# Better approaches for large ranges
large_range.each { |n| process(n) } # Memory efficient iteration
large_range.step(1000) { |n| process(n) } # Process subset of values
large_range.include?(500_000) # Constant time membership
# Infinite ranges with enumeration methods
endless_range = 1..
# endless_range.to_a # Never terminates!
# endless_range.size # Returns Float::INFINITY
# Safe operations with infinite ranges
endless_range.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
endless_range.include?(1000) # => true
Range bounds validation becomes critical when creating ranges dynamically. Invalid ranges occur when the start value is greater than the end value for comparable objects. Empty ranges behave differently than expected ranges, affecting conditional logic.
# Invalid range creation
invalid_range = 10..1
invalid_range.to_a # => []
invalid_range.include?(5) # => false
invalid_range.size # => 0
# Date range validation pitfalls
require 'date'
start_date = Date.new(2023, 12, 31)
end_date = Date.new(2023, 1, 1)
date_range = start_date..end_date
date_range.empty? # => true
date_range.include?(Date.new(2023, 6, 15)) # => false
# Proper range validation
def valid_range?(start_val, end_val)
return false unless start_val.respond_to?(:<=>)
return false unless end_val.respond_to?(:<=>)
start_val <= end_val
end
# Dynamic range creation with validation
def safe_range(start_val, end_val)
if valid_range?(start_val, end_val)
start_val..end_val
else
raise ArgumentError, "Invalid range: #{start_val}..#{end_val}"
end
end
Type coercion issues arise when range bounds have different types or incompatible comparison methods. Ruby requires both range bounds to implement comparable methods consistently. Mixed-type ranges often fail during enumeration or comparison operations.
# Type mismatch in ranges
begin
mixed_range = 1..'z'
mixed_range.to_a
rescue ArgumentError => e
puts e.message # => "bad value for range"
end
# Numeric type coercion problems
float_range = 1.5..5.7
float_range.to_a # => [1.5] - only includes start value
# Integer step required for float enumeration
float_range.step(0.5).to_a
# => [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5]
# Custom object comparison failures
class IncomparableClass
def initialize(value)
@value = value
end
end
obj1 = IncomparableClass.new(1)
obj2 = IncomparableClass.new(2)
# This fails without proper comparison implementation
begin
broken_range = obj1..obj2
broken_range.include?(obj1)
rescue NoMethodError => e
puts "Range requires <=> method: #{e.message}"
end
Reference
Range Creation Operators
Operator | Type | Behavior | Example | Result |
---|---|---|---|---|
.. |
Inclusive | Includes both start and end values | 1..5 |
[1, 2, 3, 4, 5] |
... |
Exclusive | Excludes end value | 1...5 |
[1, 2, 3, 4] |
start.. |
Endless | No upper bound (Ruby 2.6+) | 10.. |
10, 11, 12, ... |
..end |
Beginless | No lower bound (Ruby 2.7+) | ..5 |
..., 3, 4, 5 |
Core Range Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each |
{block} |
Range |
Iterates over range values |
#include?(obj) |
obj (Object) |
Boolean |
Tests membership in range |
#cover?(obj) |
obj (Object) |
Boolean |
Tests if value falls within bounds |
#to_a |
None | Array |
Converts range to array |
#size |
None | Integer |
Returns number of elements |
#first |
n=nil (Integer) |
Object or Array |
Returns first element(s) |
#last |
n=nil (Integer) |
Object or Array |
Returns last element(s) |
#step(n=1) |
n (Numeric), {block} |
Enumerator |
Steps through range by increment |
#reverse_each |
{block} |
Range |
Iterates in reverse order |
#begin |
None | Object |
Returns start value |
#end |
None | Object |
Returns end value |
#exclude_end? |
None | Boolean |
Tests if range excludes end value |
#empty? |
None | Boolean |
Tests if range contains no values |
Range Predicates and Properties
Method | Returns | Description |
---|---|---|
#finite? |
Boolean |
Tests if range has finite bounds |
#infinite? |
Boolean |
Tests if range has infinite bounds |
#beginless? |
Boolean |
Tests if range has no start value |
#endless? |
Boolean |
Tests if range has no end value |
Enumeration and Conversion Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#map |
{block} |
Array |
Maps range values through block |
#select |
{block} |
Array |
Filters range values |
#reject |
{block} |
Array |
Excludes values matching block |
#take(n) |
n (Integer) |
Array |
Takes first n elements |
#drop(n) |
n (Integer) |
Array |
Skips first n elements |
#find |
{block} |
Object |
Finds first matching element |
#count |
{block} optional |
Integer |
Counts elements or matching elements |
Range Comparison Behavior
Operation | Inclusive (.. ) |
Exclusive (... ) |
Notes |
---|---|---|---|
(1..5).size |
5 |
4 |
Size differs by 1 |
(1..5).include?(5) |
true |
false |
End value inclusion |
(1..5).to_a.last |
5 |
4 |
Last element differs |
(1..5).cover?(5) |
true |
false |
Boundary coverage |
Custom Object Requirements
Range-compatible objects must implement:
Method | Purpose | Required For |
---|---|---|
<=> |
Comparison | Range creation, membership testing |
succ |
Successor | Enumeration with each , to_a |
respond_to?(:succ) |
Method detection | Enumeration capability checking |
Common Range Patterns
# Array slicing
array[start..end] # Inclusive slice
array[start...end] # Exclusive slice
array[start..-1] # From start to end
array[-3..-1] # Last 3 elements
# Case statement patterns
case value
when 1..10 # Range matching
when 11...20 # Exclusive upper bound
when 20.. # Endless range (Ruby 2.6+)
end
# Membership testing
range.include?(value) # Exact membership
range.cover?(value) # Boundary coverage
range === value # Case equality (same as cover?)
# Iteration patterns
range.each { |x| ... } # Standard iteration
range.step(n) { |x| ... } # Custom step size
range.reverse_each { |x| ... } # Reverse iteration
Error Conditions
Error | Cause | Example |
---|---|---|
ArgumentError |
Invalid range bounds | 1.5..5.7 with to_a |
NoMethodError |
Missing comparison method | Custom object without <=> |
TypeError |
Incompatible types | 1..'z' |
SystemStackError |
Infinite enumeration | (1..).to_a |
Performance Characteristics
Operation | Time Complexity | Memory Usage | Notes |
---|---|---|---|
Range creation | O(1) | O(1) | Constant time/space |
include? |
O(1) | O(1) | Direct calculation |
cover? |
O(1) | O(1) | Boundary comparison only |
to_a |
O(n) | O(n) | Creates array of all values |
each |
O(n) | O(1) | Lazy iteration |
step(k) |
O(n/k) | O(1) | Reduces iteration count |