ZetCode

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_exclusive.rb
# 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.

creating.rb
# 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.

membership.rb
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.

stepping.rb
# 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.

loops_strings.rb
# 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_step.rb
# 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.