Overview
Ruby provides range operators to create Range objects representing sequences of values between two endpoints. The language includes two primary range operators: the inclusive range operator (..
) and the exclusive range operator (...
). These operators generate Range instances that encapsulate start and end values with defined iteration behavior.
Ruby's Range class implements ranges as objects containing a beginning value, an end value, and an exclusion flag. The inclusive operator creates ranges that include the end value, while the exclusive operator excludes the end value from the sequence. Ranges work with any objects that implement the <=>
(spaceship) operator and respond to succ
for iteration.
inclusive_range = 1..10
exclusive_range = 1...10
letter_range = 'a'..'z'
The Range class integrates with Ruby's enumerable system, making ranges compatible with iteration methods, array slicing, and case statement patterns. Ranges serve multiple purposes: generating sequences, testing membership, slicing collections, and pattern matching in conditional statements.
Basic Usage
Range creation requires two values connected by either the inclusive (..
) or exclusive (...
) operator. The start value must be comparable to the end value using the <=>
operator.
# Numeric ranges
numbers = 1..5
puts numbers.to_a
# => [1, 2, 3, 4, 5]
float_range = 1.5..3.5
puts float_range.cover?(2.7)
# => true
String ranges generate character sequences based on ASCII values and string succession rules.
letters = 'a'..'e'
puts letters.to_a
# => ["a", "b", "c", "d", "e"]
# Multi-character strings
words = 'aa'..'ad'
puts words.to_a
# => ["aa", "ab", "ac", "ad"]
Array slicing uses ranges to extract subsequences. Ruby interprets range indices as positions within the array.
fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
puts fruits[1..3]
# => ["banana", "cherry", "date"]
puts fruits[1...3]
# => ["banana", "cherry"]
Case statements accept ranges as conditions, testing whether values fall within specified bounds.
grade = 85
case grade
when 90..100
puts "A grade"
when 80..89
puts "B grade"
when 70..79
puts "C grade"
else
puts "Below C"
end
# => "B grade"
Range membership testing uses include?
for discrete values and cover?
for continuous values. The include?
method iterates through range values, while cover?
performs boundary comparison.
range = 1..10
puts range.include?(5)
# => true
puts range.cover?(5.5)
# => true
puts range.include?(5.5)
# => false
Advanced Usage
Ranges support complex iteration patterns through enumerable methods. The step
method creates custom increments for numeric ranges.
# Stepping through ranges
(1..20).step(3) do |n|
puts n
end
# => 1, 4, 7, 10, 13, 16, 19
# Reverse iteration
10.downto(1).step(2).to_a
# => [10, 8, 6, 4, 2]
Custom objects integrate with ranges by implementing <=>
and succ
. The spaceship operator enables comparison, while succ
defines succession logic.
class Version
include Comparable
attr_reader :major, :minor
def initialize(major, minor)
@major, @minor = major, minor
end
def <=>(other)
[major, minor] <=> [other.major, other.minor]
end
def succ
if minor < 9
Version.new(major, minor + 1)
else
Version.new(major + 1, 0)
end
end
def to_s
"#{major}.#{minor}"
end
end
version_range = Version.new(1, 0)..Version.new(1, 3)
puts version_range.map(&:to_s)
# => ["1.0", "1.1", "1.2", "1.3"]
Infinite ranges represent unbounded sequences using nil
as the endpoint. Ruby supports beginless and endless ranges for flexible pattern matching.
# Endless range
positive_numbers = 1..
puts positive_numbers.cover?(1000)
# => true
# Beginless range
up_to_ten = ..10
puts up_to_ten.cover?(5)
# => true
# Pattern matching with infinite ranges
age = 25
case age
when ..17
puts "Minor"
when 18..64
puts "Adult"
when 65..
puts "Senior"
end
# => "Adult"
Range composition enables complex filtering and selection logic through method chaining with enumerable operations.
# Complex range operations
data = (1..100).select { |n| n % 7 == 0 }
.reject { |n| (20..30).cover?(n) }
.map { |n| n * 2 }
puts data.first(5)
# => [14, 70, 98, 126, 154]
# Multi-dimensional range filtering
coordinates = (1..5).flat_map { |x| (1..5).map { |y| [x, y] } }
filtered = coordinates.select { |x, y| (2..4).cover?(x) && (2..4).cover?(y) }
puts filtered
# => [[2, 2], [2, 3], [2, 4], [3, 2], [3, 3], [3, 4], [4, 2], [4, 3], [4, 4]]
Common Pitfalls
String ranges exhibit unexpected behavior when mixing single and multi-character strings. Ruby treats multi-character strings as sequences, leading to exponential expansion.
# Dangerous string range
large_range = 'a'..'zzz'
puts large_range.count
# => 475254 (very large number)
# Safer approach for string patterns
pattern_range = ('a'..'z').map { |c| c * 3 }
puts pattern_range.first(5)
# => ["aaa", "bbb", "ccc", "ddd", "eee"]
Float ranges with inclusive operators create precision issues due to floating-point representation limitations.
# Problematic float iteration
range = 0.1..0.3
puts range.step(0.1).to_a
# => [0.1, 0.2, 0.30000000000000004]
# Better approach with rational numbers
require 'rational'
range = Rational(1, 10)..Rational(3, 10)
puts range.step(Rational(1, 10)).to_a
# => [(1/10), (1/5), (3/10)]
Range comparison behavior differs between include?
and cover?
methods. The include?
method requires iteration through discrete values, while cover?
performs boundary checking.
string_range = 'a'..'z'
# include? iterates through all values
puts string_range.include?('m')
# => true
puts string_range.include?('hello')
# => false (not a single character)
# cover? only checks boundaries
puts string_range.cover?('hello')
# => true (falls between 'a' and 'z' lexicographically)
Array slicing with ranges produces different results when ranges exceed array bounds. Ruby handles out-of-bounds ranges gracefully but inconsistently.
arr = [1, 2, 3]
# Ranges starting beyond array length
puts arr[10..15]
# => nil
# Ranges ending beyond array length
puts arr[1..10]
# => [2, 3]
# Negative indices in ranges
puts arr[-2..-1]
# => [2, 3]
puts arr[-10..-1]
# => nil (start index too negative)
Time ranges require careful handling of time zones and precision. Time objects compare using internal representations, which may not match expected behavior across time zone boundaries.
require 'time'
start_time = Time.parse("2024-01-01 00:00:00 UTC")
end_time = Time.parse("2024-01-01 23:59:59 UTC")
time_range = start_time..end_time
# Time comparison respects time zones
local_time = Time.parse("2024-01-01 12:00:00 -0500")
puts time_range.cover?(local_time)
# => true (converted to UTC internally)
# Precision issues with sub-second times
precise_time = Time.now
puts (precise_time..precise_time).include?(precise_time)
# => true
similar_time = Time.at(precise_time.to_f + 0.0001)
puts (precise_time..precise_time).include?(similar_time)
# => false
Reference
Range Creation Methods
Syntax | Type | Includes End | Example |
---|---|---|---|
start..end |
Inclusive | Yes | 1..5 |
start...end |
Exclusive | No | 1...5 |
Range.new(start, end) |
Inclusive | Yes | Range.new(1, 5) |
Range.new(start, end, true) |
Exclusive | No | Range.new(1, 5, true) |
Core Range Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#begin |
None | Object |
Returns range start value |
#end |
None | Object |
Returns range end value |
#exclude_end? |
None | Boolean |
Tests if range excludes end value |
#include?(obj) |
obj (Object) |
Boolean |
Tests membership by iteration |
#cover?(obj) |
obj (Object) |
Boolean |
Tests membership by comparison |
#size |
None | Integer |
Returns range element count |
#to_a |
None | Array |
Converts range to array |
#each |
Block | Enumerator |
Iterates through range values |
#step(n=1) |
n (Numeric), Block |
Enumerator |
Iterates with custom step size |
Range Iteration Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#map |
Block | Array |
Transforms each range element |
#select |
Block | Array |
Filters range elements |
#reject |
Block | Array |
Excludes matching elements |
#find |
Block | Object |
Returns first matching element |
#count |
Block (optional) | Integer |
Counts elements or matches |
#any? |
Block (optional) | Boolean |
Tests if any element matches |
#all? |
Block (optional) | Boolean |
Tests if all elements match |
Special Range Types
Type | Syntax | Example | Use Case |
---|---|---|---|
Endless | start.. |
1.. |
Unbounded upper limit |
Beginless | ..end |
..10 |
Unbounded lower limit |
Infinite | nil..nil |
Not recommended | Complete unbounded range |
Range Comparison Behavior
Method | Iteration Required | Type Support | Performance |
---|---|---|---|
#include? |
Yes | Discrete only | O(n) |
#cover? |
No | Continuous and discrete | O(1) |
#=== |
No | Uses cover? internally |
O(1) |
Common Range Errors
Error | Cause | Example |
---|---|---|
ArgumentError |
Invalid range boundaries | 10..1 |
TypeError |
Non-comparable objects | 1..'a' |
NoMethodError |
Missing succ method |
Custom objects without succ |
SystemStackError |
Infinite string ranges | 'a'..'zzz' iteration |