Ruby Ranges
last modified October 18, 2026
A range represents an interval of values in Ruby. Ranges can be inclusive
(..) or exclusive (...), can be endless or beginless,
and can be iterated, tested for membership, and stepped through. This guide
covers the common range operations with examples that were run on Ruby 3.0.2.
Inclusive and Exclusive Ranges
The two-dot form 1..5 includes the end value, while the three-dot
form 1...5 excludes it. The exclude_end? method tells
you which form a range uses.
# Inclusive range: includes the end value
inclusive = (1..5)
puts "1..5 -> #{inclusive.to_a.inspect}"
# Exclusive range: excludes the end value
exclusive = (1...5)
puts "1...5 -> #{exclusive.to_a.inspect}"
# Size and endpoints
puts "inclusive size: #{inclusive.size}, first: #{inclusive.first}, last: #{inclusive.last}"
puts "exclusive size: #{exclusive.size}, first: #{exclusive.first}, last: #{exclusive.last}"
puts "exclusive end: #{exclusive.end}, exclude_end?: #{exclusive.exclude_end?}"
# Ranges with other types
puts (1.0..2.0).include?(2.0)
puts (1.0...2.0).include?(2.0)
to_a shows exactly which values a range produces. With the
exclusive form, the end value 5 is absent. The last example shows the same
distinction for float ranges, where 2.0 is included only by the
two-dot form.
$ ./inclusive_exclusive.rb 1..5 -> [1, 2, 3, 4, 5] 1...5 -> [1, 2, 3, 4] inclusive size: 5, first: 1, last: 5 exclusive size: 4, first: 1, last: 5 exclusive end: 5, exclude_end?: true true false
Creating Ranges
Ranges are usually created with literal syntax, but Range.new is
equivalent. Its optional third argument sets the exclusive flag. The endless
range (1..) has no upper bound, and the beginless range
(..5) has no lower bound; both are useful with lazy enumeration and
comparisons.
# Literal syntax and Range.new are equivalent
r1 = 1..10
r2 = Range.new(1, 10) # inclusive
r3 = Range.new(1, 10, true) # exclusive
puts r1.inspect
puts r3.inspect
puts r1 == r2
puts r3.to_a.last
# Endless range (no upper bound)
endless = (1..)
puts endless.inspect
puts endless.first(3).inspect
puts endless.lazy.map { |x| x * x }.first(4).inspect
# Beginless range (no lower bound)
beginless = (..5)
puts beginless.inspect
puts beginless.include?(-100)
puts beginless.end
puts ('a'..'e').to_a.inspect
Range.new(1, 10, true) produces the same exclusive range as
1...10. The endless range prints as 1.., and
first(3) takes only the requested elements, which is why it works
without an upper bound. The beginless range prints as ..5.
$ ./creating.rb 1..10 1...10 true 9 1.. [1, 2, 3] [1, 4, 9, 16] ..5 true 5 ["a", "b", "c", "d", "e"]
Membership, cover? and ===
include? and member? test membership. For numeric
ranges, cover? checks whether a value or a whole range falls
between the bounds. The === operator is what case/when
uses, so it is also how ranges are matched in conditionals. For string ranges,
include? iterates the range, while cover? and
=== compare by bounds.
range = (1..10)
exclusive = (1...10)
puts exclusive.include?(10) # false
puts range.cover?(3..7) # true
puts range === 7 # true
def grade(score)
case score
when 90..100 then "A"
when 80...90 then "B"
else "Invalid score"
end
end
puts grade(85)
puts grade(101)
letters = ('a'..'z')
puts letters.include?("mm") # false
puts letters.cover?("mm") # true
The exclusive range rejects 10, while the inclusive one accepts it.
cover? accepts another range (3..7) as well as a
single value. The grade method uses ranges in case/when
to classify scores, and the final lines show why "mm" is in
cover? and === but not include? for the
string range 'a'..'z'.
$ ./membership.rb false true true B Invalid score false true
Iterating and Stepping
each visits every value in an integer or string range. The
step method visits every n-th value and returns an enumerator
when no block is given. The % operator is an alias for
step. Ranges also support reverse_each for
descending traversal.
# step with an explicit increment
(1..10).step(3) { |n| print n, " " }
puts
# The % operator is an alias for step
puts ((1..10) % 4).to_a.inspect
# Exclusive range with step
puts (0...10).step(2).to_a.inspect
# Stepping returns an enumerator
e = (1..20).step(5)
puts e.class
puts e.to_a.inspect
# Stepping backwards is not possible with a positive range; use reverse_each
puts (1..10).reverse_each.first(3).inspect
Stepping a range with a block prints every third value. The %
form gives the same result as step. Without a block,
step returns an Enumerator::ArithmeticSequence, which
can be converted with to_a. A range cannot be stepped backwards,
so reverse_each is used instead.
$ ./stepping.rb 1 4 7 10 [1, 5, 9] [0, 2, 4, 6, 8] Enumerator::ArithmeticSequence [1, 6, 11, 16] [10, 9, 8]
Looping Over Ranges
Integer and string ranges work directly with each. String ranges
expand using the string successor rules, so 'a'..'e' yields
a through e, and 'aa'..'ad' yields the
two-letter sequence between those bounds.
# Iterating an inclusive range with each
(1..3).each { |i| puts "Number #{i}" }
# Iterating an exclusive range
(1...3).each { |i| puts "Index #{i}" }
# Iterating a string range
('a'..'e').each { |ch| print ch, " " }
puts
# Using each_with_index on a range
('x'..'z').each_with_index do |letter, idx|
puts "#{idx}: #{letter}"
end
# String range expansion
puts ('aa'..'ad').to_a.inspect
puts ('A'..'E').map(&:downcase).join(",")
The exclusive range 1...3 stops before 3. The string range
'a'..'e' produces five letters, and each_with_index
pairs each element with its position. map(&:downcase) shows
that range elements can be transformed like any other enumerable.
$ ./loops_strings.rb Number 1 Number 2 Number 3 Index 1 Index 2 a b c d e 0: x 1: y 2: z ["aa", "ab", "ac", "ad"] a,b,c,d,e
Float Ranges and Step
Float ranges cannot be iterated with each because a float range
has no discrete successor. They do support step with a float
increment, and include? and cover? work as usual.
Float steps accumulate rounding error, as the output below shows.
# Float ranges cannot be iterated with each, but step works
(0.0..1.0).step(0.25) { |x| print x, " " }
puts
# Float range membership
r = (0.0..1.0)
puts r.include?(0.5)
puts r.cover?(1.0)
# Exclusive float range
puts (0.0...1.0).include?(1.0)
# Collect float steps into an array
values = (0.0..0.5).step(0.1).to_a
puts values.inspect
# Using % with floats
puts ((1.0..2.0) % 0.5).to_a.inspect
The value 0.30000000000000004 in the output comes from binary
floating-point arithmetic, not from Ruby's range logic. For exact decimal
values, use Rational or integer scaling instead of float steps.
$ ./float_step.rb 0.0 0.25 0.5 0.75 1.0 true true false [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5] [1.0, 1.5, 2.0]
Source References
Learn more from these resources: Ruby Range Documentation, Range Literals, and ArithmeticSequence.
List all Ruby tutorials.
Author
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.