Ranges

Reviewed & published by Brayan K

Ruby is a dynamic, beginner-friendly programming language, and a Range is a compact way to express a sequence between two endpoints — perfect for loops, slicing, and bucketing values.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll build inclusive and exclusive ranges, iterate them, and use them in case statements and slices.

What You'll Learn in This Lesson

1️⃣ Building and Iterating Ranges

Two dots 1..5 include the end; three dots 1...5 exclude it. Ranges respond to all the Enumerable methods you've seen — each, sum, include? — and you can turn one into an array with to_a.

# A Range expresses a sequence between two endpoints.
# Two dots .. INCLUDE the end; three dots ... EXCLUDE it.
inclusive = (1..5)
exclusive = (1...5)

puts inclusive.to_a.inspect   # -> [1, 2, 3, 4, 5]
puts exclusive.to_a.inspect   # -> [1, 2, 3, 4]

# Ranges loop nicely with each:
(1..3).each { |n| puts "Count #{n}" }   # -> Count 1, 2, 3

# Letters work too:
puts ("a".."e").to_a.inspect  # -> ["a", "b", "c", "d", "e"]

# Handy aggregate methods come from Enumerable:
puts (1..100).sum             # -> 5050
puts (1..10).include?(7)      # -> true

2️⃣ Ranges in case, step, and Slicing

Ranges make case/when read beautifully for buckets like grades or age brackets. cover? checks membership instantly without building a list, step walks in increments, and a range can slice arrays and strings.

# Ranges shine in case/when for bucketing values.
score = 73
grade = case score
        when 90..100 then "A"
        when 80...90 then "B"
        when 70...80 then "C"
        else "F"
        end
puts grade                    # -> C

# cover? tests membership without building an array (fast):
puts (1..1_000_000).cover?(500_000)   # -> true

# step walks a range in increments:
(0..10).step(2) { |n| print "#{n} " }  # -> 0 2 4 6 8 10
puts

# Ranges also slice arrays and strings:
letters = ["a", "b", "c", "d", "e"]
puts letters[1..3].inspect    # -> ["b", "c", "d"]
puts "Hello"[0..2]            # -> Hel

Your turn. Print a times table and a sum using ranges. The TODOs remind you to keep the range inclusive — run it for a few values of n.

# 🎯 YOUR TURN — replace each TODO, then run with: ruby times_table.rb

# Build a times table for a chosen number.
n = 5      # 👉 change to any number, e.g. 7

# 1) Loop over the range 1 to 10 (inclusive)
(1..10).each do |i|        # 👉 TODO: make sure this includes 10
  product = n * i
  puts "#{n} x #{i} = #{product}"
end

# 2) Print the SUM of 1..n using a range's .sum
puts "Sum 1..#{n} = #{(1..n).sum}"

# ✅ Expected output (n = 5):
#    5 x 1 = 5
#    ...
#    5 x 10 = 50
#    Sum 1..5 = 15

Mini-Challenge: Even Numbers in a Range

Filter a range down to its even numbers, then sum them. This previews select from Iterators & Enumerable. Run with ruby evens.rb.

# 🎯 MINI-CHALLENGE: an even-number filter.
# Save as evens.rb and run with: ruby evens.rb
#
# 1. Take the range 1..20.
# 2. Convert it to an array and keep only even numbers
#    (hint: .select { |n| n.even? }).
# 3. Print the resulting array and its sum.
#
# ✅ Example output:
#    [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
#    Sum: 110

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Ranges

ConceptRuby Syntax
Inclusive(1..5)
Exclusive(1...5)
To array(1..5).to_a
Membership(1..9).cover?(5)
Step(0..10).step(2)
Slicearr[1..3]

🎉 Lesson Complete!

Practice quiz

What does (1..5).to_a return?

  • [1, 2, 3, 4]
  • [1, 2, 3, 4, 5]
  • [2, 3, 4, 5]
  • [1, 5]

Answer: [1, 2, 3, 4, 5]. Two dots .. include the end, so (1..5) covers 1 through 5.

What does (1...5).to_a return?

  • [1, 2, 3, 4, 5]
  • [1, 2, 3, 4]
  • [2, 3, 4, 5]
  • [1, 5]

Answer: [1, 2, 3, 4]. Three dots ... exclude the end, so (1...5) covers 1 through 4.

What does (1..100).sum return?

  • 100
  • 5000
  • 5050
  • 10100

Answer: 5050. Ranges respond to Enumerable methods; (1..100).sum is 5050.

What does (1..1_000_000).cover?(500_000) return?

  • true
  • false
  • nil
  • an error

Answer: true. cover? checks membership instantly from the endpoints without building a list.

What does (0..10).step(2).to_a produce?

  • [0, 2, 4, 6, 8]
  • [0, 2, 4, 6, 8, 10]
  • [2, 4, 6, 8, 10]
  • [0, 1, 2]

Answer: [0, 2, 4, 6, 8, 10]. step(2) walks the inclusive range in increments of 2: 0, 2, 4, 6, 8, 10.

What does (5..1).to_a return?

  • [5, 4, 3, 2, 1]
  • [1, 2, 3, 4, 5]
  • [] (empty array)
  • nil

Answer: [] (empty array). A range with start greater than end is empty, not a countdown.

What does "Hello"[0..2] return?

  • "He"
  • "Hel"
  • "Hello"
  • "ello"

Answer: "Hel". A range slices the string by index 0 through 2 inclusive, giving "Hel".

Why is cover? often preferred over include? for very large ranges?

  • cover? is O(1) on endpoints; include? may scan
  • include? doesn't exist
  • cover? builds an array
  • They are identical

Answer: cover? is O(1) on endpoints; include? may scan. cover? checks endpoints in O(1) while include? may iterate.

Which kind of object can form a range besides numbers?

  • Only floats
  • Letters/strings like ("a".."e")
  • Only symbols
  • Hashes

Answer: Letters/strings like ("a".."e"). Any comparable object with a successor works, e.g. letter ranges ("a".."e").

In a case/when, which range matches a score of 73 among 90..100, 80...90, 70...80?

  • 90..100
  • 80...90
  • 70...80
  • none

Answer: 70...80. 73 falls in 70...80 (exclusive of 80), which Ruby matches with ===.

Continue this course

Frequently asked questions

What's the difference between .. and ... in a range?

Two dots (1..5) create an inclusive range that contains the end value, so 1..5 covers 1, 2, 3, 4, and 5. Three dots (1...5) create an exclusive range that stops just before the end, so it covers 1, 2, 3, and 4. The exclusive form is especially handy for array indexing, where 0...arr.length lines up exactly with the valid indexes 0 through length-1.

Are ranges stored as full lists in memory?

No, and that's a key benefit. A range like (1..1_000_000) stores only its two endpoints, not a million numbers. It generates values lazily as you iterate, which makes ranges extremely memory-efficient. Methods like cover? check membership instantly using the endpoints without building anything. Only when you call to_a (or another method that materialises it) does Ruby actually create the full array.

Can I use ranges in case/when statements?

Yes, and it's one of their best uses. A when clause can hold a range, like when 80...90 then "B", and Ruby checks membership with the === operator behind the scenes. This makes grading, age brackets, and price tiers read clearly. Just watch your inclusive vs exclusive boundaries so adjacent buckets (like 80...90 and 90..100) don't overlap or leave gaps.

Do ranges only work with numbers?

No. Ranges work with any objects that understand ordering and 'successor'. The most common non-numeric range is letters: ("a".."z") gives the alphabet. Ranges of dates, times, and even custom objects work if they implement the right comparison methods. For iterating, an object needs a succ method; for membership tests with cover?, it just needs to be comparable with <=>.

Related lessons