Regular Expressions

Reviewed & published by Brayan K

Ruby is a dynamic, beginner-friendly programming language with excellent built-in regular expressions — compact patterns for matching, extracting, and replacing text.

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 write patterns, test matches, capture pieces of text, and transform strings with gsub and scan.

What You'll Learn in This Lesson

1️⃣ Matching: Patterns, Classes, and Anchors

A pattern lives between slashes: /world/. Test it with match? (true/false) or =~ (index or nil). The building blocks are character classes like \d (digit) and \w (word char), anchors ^/$, and quantifiers like +.

# A regex (pattern) is written between slashes: /pattern/
# The =~ operator tests for a match and returns the index, or nil.
puts ("hello world" =~ /world/)    # -> 6   (match starts at index 6)
puts ("hello" =~ /xyz/).inspect    # -> nil (no match)

# match? returns a simple true/false (fast, preferred for yes/no):
puts "cat".match?(/c.t/)           # -> true   . matches any one char
puts "dog".match?(/^d/)            # -> true   ^ anchors to the start
puts "dog".match?(/g$/)            # -> true   $ anchors to the end

# Character classes and quantifiers:
puts "a1b2".match?(/[0-9]/)        # -> true   [0-9] = any digit
puts "abc123".match?(/\d+/)         # -> true   \d = digit, + = one or more
puts "   ".match?(/\s/)             # -> true   \s = whitespace
puts "hello".match?(/\w+/)          # -> true   \w = word character

2️⃣ Extracting and Transforming

Wrap parts of a pattern in () to capture them, then read data[1], data[2], and so on. scan collects every match into an array; gsub replaces them all; and split can break a string on a pattern.

# Capture groups () pull pieces out of a match.
text = "Order 42 shipped on 2026-06-17"

if data = text.match(/(\d{4})-(\d{2})-(\d{2})/)
  puts data[0]    # -> 2026-06-17  (the whole match)
  puts data[1]    # -> 2026        (first group)
  puts data[2]    # -> 06          (second group)
end

# scan finds ALL matches and returns them in an array:
puts "cat bat rat".scan(/\w+at/).inspect   # -> ["cat", "bat", "rat"]

# gsub replaces every match (sub does just the first):
puts "2026-06-17".gsub(/-/, "/")          # -> 2026/06/17
puts "Hello, World".gsub(/[aeiou]/i, "*") # -> H*ll*, W*rld  (i = ignore case)

# split on a pattern:
puts "a1b2c3".split(/\d/).inspect         # -> ["a", "b", "c"]

Your turn. Use a pattern to validate emails and extract a domain. The pattern is supplied — read the TODO note to understand each piece, then run it.

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

# A (very simplified) email checker.
def valid_email?(text)
  # 👉 TODO: the pattern needs: some word chars, @, more word chars,
  #    a dot, and 2+ letters. \w means word char, . is a literal dot here.
  text.match?(/\A\w+@\w+\.[a-z]{2,}\z/)
end

puts valid_email?("ada@example.com")   # -> true
puts valid_email?("not-an-email")      # -> false
puts valid_email?("bo@site.io")        # -> true

# Bonus: extract the domain with a capture group
m = "ada@example.com".match(/@(\w+\.\w+)/)
puts "Domain: #{m[1]}" if m            # -> Domain: example.com

# ✅ Expected output:
#    true
#    false
#    true
#    Domain: example.com

Mini-Challenge: Extract Phone Numbers

Use scan with a digit pattern to pull every phone number out of a sentence. Run with ruby phones.rb.

# 🎯 MINI-CHALLENGE: extract all phone numbers.
# Save as phones.rb and run with: ruby phones.rb
#
# Given text with numbers like "555-1234" mixed in:
#   text = "Call 555-1234 or 555-9876 today!"
# 1. Use scan with a pattern \d{3}-\d{4} to find all phone numbers.
# 2. Print how many you found and the list itself.
#
# ✅ Example output:
#    Found 2: ["555-1234", "555-9876"]

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Regular Expressions

ConceptRuby Syntax
Test matchs.match?(/pat/)
Digit / word / space\d \w \s
One+ / optional+ ? * {2,4}
Captures.match(/(\d+)/)[1]
All matchess.scan(/\w+/)
Replace alls.gsub(/x/, "y")

🎉 Lesson Complete!

Practice quiz

What does ("hello world" =~ /world/) return?

  • true
  • 0
  • 6
  • nil

Answer: 6. =~ returns the index where the match starts; "world" begins at index 6.

What does ("hello" =~ /xyz/) return?

  • nil
  • false
  • 0
  • -1

Answer: nil. =~ returns nil when there is no match.

What does the character class \d match?

  • Any word character
  • Whitespace
  • Any letter
  • Any digit

Answer: Any digit. \d matches any single digit 0-9.

What does \w match?

  • Whitespace only
  • A word character (letters, digits, underscore)
  • Only letters
  • Any character

Answer: A word character (letters, digits, underscore). \w matches a word character: letters, digits, or underscore.

Which method returns a simple true/false for a match?

  • match?
  • scan
  • match
  • =~

Answer: match?. match? returns a boolean and is the fastest yes/no check.

After m = "2026-06-17".match(/(\d{4})-(\d{2})-(\d{2})/), what is m[1]?

  • "2026-06-17"
  • "06"
  • "2026"
  • "17"

Answer: "2026". m[0] is the whole match; m[1] is the first capture group, "2026".

What does "cat bat rat".scan(/\w+at/) return?

  • "cat"
  • ["cat", "bat", "rat"]
  • 3
  • nil

Answer: ["cat", "bat", "rat"]. scan finds all matches and returns them in an array.

What does "2026-06-17".gsub(/-/, "/") return?

  • "2026-06-17"
  • "2026/06-17"
  • nil
  • "2026/06/17"

Answer: "2026/06/17". gsub replaces every match, turning all dashes into slashes.

What does "a1b2c3".split(/\d/) return?

  • ["a1", "b2", "c3"]
  • ["a", "b", "c"]
  • ["1", "2", "3"]
  • ["abc"]

Answer: ["a", "b", "c"]. split breaks the string on each digit, leaving the letters.

Which flag makes a pattern match case-insensitively?

  • /pat/g
  • /pat/m
  • /pat/i
  • /pat/x

Answer: /pat/i. The i flag, as in /cat/i, matches case-insensitively.

Continue this course

Frequently asked questions

How do I write and test a regex in Ruby?

A regular expression literal goes between slashes, like /\d+/. To test whether a string matches, use match? for a boolean ("abc123".match?(/\d/) returns true) — it's the fastest and clearest for yes/no checks. The older =~ operator returns the index of the first match or nil, which is handy in conditionals. For pulling data out, use match (returns a MatchData object with capture groups) or scan (returns an array of all matches).

What do \d, \w, \s, and the anchors mean?

These are character shortcuts: \d matches any digit, \w matches a word character (letters, digits, underscore), and \s matches whitespace. Their uppercase versions (\D, \W, \S) match the opposite. Anchors pin a match to a position: ^ and \A mean start, $ and \z mean end. Quantifiers say how many: + is one or more, * is zero or more, ? is optional, and {2,4} is between two and four. Combining these — like \A\d{4}\z for exactly four digits — is how you build precise patterns.

How do capture groups work?

Parentheses in a pattern create capture groups that remember the matched substring. After a successful match, the MatchData object lets you read them by number: data[0] is the entire match, data[1] is the first group, data[2] the second, and so on. So /(\d{4})-(\d{2})-(\d{2})/ against a date gives you the year, month, and day separately. You can also use named groups like (?<year>\d{4}) and read them with data[:year] for clearer code.

What's the difference between sub, gsub, and scan?

sub replaces only the first match of the pattern; gsub (global sub) replaces every match. Both return a new string and take either a replacement string or a block for computed replacements. scan doesn't replace anything — it finds and returns all matches as an array, which is perfect for extracting every email, number, or word that fits a pattern. As a rule: gsub to transform text, scan to collect matches, and sub when you specifically want just the first occurrence changed.