Regular Expressions in Ruby
Why Learn Regular Expressions?
Regular expressions (regex) are powerful pattern-matching tools that solve critical text processing problems:
- Data Validation: Verify email addresses, phone numbers, credit cards, passwords
- Text Parsing: Extract specific information from logs, documents, or user input
- Data Cleaning: Remove unwanted characters, normalize formats, fix inconsistencies
- Search & Replace: Find and modify complex patterns in large text files
- URL Routing: Match and extract parameters from web application routes
Problems Regular Expressions Solve
1. Input Validation
Instead of writing complex if-else logic to validate formats, regex provides a concise pattern-based approach. One regex can validate email formats that would require dozens of lines of manual string checking.
2. Data Extraction
Extract specific information from unstructured text like log files, CSV data, or user-generated content. Parse dates, URLs, phone numbers, or any structured data embedded in text.
3. Text Transformation
Replace complex patterns with new content, clean up formatting, or standardize data formats. Convert between different date formats, normalize phone numbers, or sanitize HTML.
Learning Path
1. Creating Regular Expressions
Basic Syntax
Ruby provides two ways to create regular expressions. The literal syntax /pattern/ is most common,
while Regexp.new() is useful when the pattern comes from a variable or needs dynamic construction.
# Literal syntax - most common
regex = /hello/
# Constructor syntax - useful for dynamic patterns
pattern = "hello"
regex = Regexp.new(pattern)
# With modifiers
regex = /hello/i # case-insensitive
regex = /hello/m # multiline mode
regex = /hello/x # extended (ignore whitespace)
# Combining modifiers
regex = /hello/im # case-insensitive AND multiline
💡 When to Use Each Approach
- Literal syntax: When the pattern is known at code-writing time
- Constructor: When building patterns from user input or configuration
- Extended mode (/x): For complex patterns that need comments and formatting
2. Pattern Matching Methods
Different Ways to Test Patterns
text = "Hello, Ruby Developer!"
# 1. Match operator =~ (returns index or nil)
puts text =~ /Ruby/ # => 7 (position of match)
puts text =~ /Python/ # => nil (no match)
# 2. String#match method (returns MatchData object)
match_result = text.match(/Ruby/)
puts match_result # => #<MatchData "Ruby">
puts match_result[0] # => "Ruby" (the matched text)
# 3. String#match? method (returns boolean - Ruby 2.4+)
puts text.match?(/Ruby/) # => true
puts text.match?(/Python/) # => false
# 4. Regex#=== method (useful in case statements)
case text
when /Ruby/
puts "Found Ruby!"
when /Python/
puts "Found Python!"
else
puts "Unknown language"
end
⚠️ Performance Considerations
Use match? when you only need to test if a pattern exists (fastest).
Use match when you need the matched text or capture groups.
Use =~ when you need the position of the match.
3. Character Classes & Quantifiers
Character Classes - Matching Types of Characters
Character classes let you match categories of characters instead of specific letters. This makes patterns flexible and reusable.
# Predefined character classes
/\d/ # Any digit (0-9)
/\w/ # Any word character (letters, digits, underscore)
/\s/ # Any whitespace (space, tab, newline)
/./ # Any character except newline
# Negated classes (uppercase = opposite)
/\D/ # Any non-digit
/\W/ # Any non-word character
/\S/ # Any non-whitespace
# Custom character classes
/[aeiou]/ # Any vowel
/[a-z]/ # Any lowercase letter
/[A-Z]/ # Any uppercase letter
/[0-9]/ # Any digit (same as \d)
/[a-zA-Z0-9]/ # Any alphanumeric character
/[^0-9]/ # Any non-digit (^ means "not")
# Real-world examples
email = "user@example.com"
puts email.scan(/\w+/) # => ["user", "example", "com"]
phone = "Call me at 123-456-7890"
puts phone.scan(/\d+/) # => ["123", "456", "7890"]
Quantifiers - How Many Times?
Quantifiers specify how many times a pattern should match. They're essential for validating formats and extracting variable-length data.
# Basic quantifiers
/* # Zero or more
/+ # One or more
/? # Zero or one (optional)
# Specific counts
/{3}/ # Exactly 3 times
/{3,}/ # 3 or more times
/{3,7}/ # Between 3 and 7 times
# Practical examples
# Phone number: exactly 3 digits, dash, 3 digits, dash, 4 digits
phone_pattern = /\d{3}-\d{3}-\d{4}/
puts "123-456-7890".match?(phone_pattern) # => true
puts "12-34-567".match?(phone_pattern) # => false
# Password: at least 8 characters with letters and numbers
password_pattern = /^(?=.*[a-z])(?=.*\d)[a-z\d]{8,}$/i
puts "password123".match?(password_pattern) # => true
puts "pass".match?(password_pattern) # => false
# Flexible name matching
name_pattern = /^[A-Z][a-z]+$/ # Capital letter followed by lowercase
puts "John".match?(name_pattern) # => true
puts "JOHN".match?(name_pattern) # => false
🎯 Greedy vs Non-Greedy Matching
By default, quantifiers are "greedy" - they match as much as possible. Add ? after a quantifier to make it "non-greedy" (match as little as possible).
html = "<div>content</div>"
puts html.match(/<.+>/)[0] # => "<div>content</div>" (greedy)
puts html.match(/<.+?>/)[0] # => "<div>" (non-greedy)
4. Search and Replace Operations
gsub and sub Methods
Ruby's gsub (global substitute) and sub (substitute once) methods are powerful tools for text transformation.
They can replace simple strings or complex patterns with new content.
# Basic replacement
text = "Hello Ruby, Ruby is great!"
puts text.gsub(/Ruby/, "Python") # => "Hello Python, Python is great!"
puts text.sub(/Ruby/, "Python") # => "Hello Python, Ruby is great!" (only first)
# Case-insensitive replacement
text = "HELLO hello HeLLo"
puts text.gsub(/hello/i, "hi") # => "hi hi hi"
# Using blocks for dynamic replacement
text = "I have 5 apples and 10 oranges"
result = text.gsub(/\d+/) do |number|
(number.to_i * 2).to_s
end
puts result # => "I have 10 apples and 20 oranges"
# Real-world examples
# Clean phone numbers
phone = "(123) 456-7890"
clean = phone.gsub(/[^\d]/, "") # Remove non-digits
puts clean # => "1234567890"
# Format currency
prices = "Price: $19.99, Sale: $15.50"
formatted = prices.gsub(/\$(\d+\.\d+)/, 'US Dollar \1')
puts formatted # => "Price: US Dollar 19.99, Sale: US Dollar 15.50"
The scan Method - Finding All Matches
The scan method finds all matches of a pattern and returns them as an array. It's perfect for extracting data from text.
# Extract all numbers from text
text = "Order #123 for $45.99 due 2024-03-15"
numbers = text.scan(/\d+/)
puts numbers # => ["123", "45", "99", "2024", "03", "15"]
# Extract email addresses
content = "Contact us at help@example.com or sales@company.org"
emails = content.scan(/\w+@\w+\.\w+/)
puts emails # => ["help@example.com", "sales@company.org"]
# Extract URLs
html = 'Visit <a href="https://example.com">our site</a> or <a href="http://test.org">test</a>'
urls = html.scan(/https?:\/\/[^\s"]+/)
puts urls # => ["https://example.com", "http://test.org"]
# Parse log entries
log = "2024-03-15 ERROR User login failed for user@domain.com"
parts = log.scan(/(\d{4}-\d{2}-\d{2}) (\w+) (.+)/)
puts parts # => [["2024-03-15", "ERROR", "User login failed for user@domain.com"]]
5. Capture Groups and Data Extraction
Understanding Capture Groups
Capture groups (parentheses in regex) let you extract specific parts of a match. This is crucial for parsing structured data like dates, URLs, or formatted text.
# Basic capture groups
date_pattern = /(\d{4})-(\d{2})-(\d{2})/
date = "Today is 2024-03-15"
match = date.match(date_pattern)
puts match[0] # => "2024-03-15" (full match)
puts match[1] # => "2024" (first group - year)
puts match[2] # => "03" (second group - month)
puts match[3] # => "15" (third group - day)
# Named capture groups (Ruby 2.0+)
email_pattern = /(?<username>\w+)@(?<domain>\w+\.\w+)/
email = "Contact: john@example.com"
match = email.match(email_pattern)
puts match[:username] # => "john"
puts match[:domain] # => "example.com"
puts match['username'] # Also works with strings
# Parsing URLs
url_pattern = /^(?<protocol>https?):\/\/(?<host>[^\/]+)(?<path>\/.*)?$/
url = "https://example.com/users/123"
parts = url.match(url_pattern)
puts parts[:protocol] # => "https"
puts parts[:host] # => "example.com"
puts parts[:path] # => "/users/123"
💡 Pro Tip: Non-Capturing Groups
Use (?:pattern) for grouping without capturing when you need parentheses for precedence but don't want to extract the content. This improves performance and keeps numbered groups clean.
Real-World Applications
Email Validation
# Simple email validation
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
def valid_email?(email)
email.match?(email_regex)
end
puts valid_email?("user@example.com") # true
puts valid_email?("invalid.email") # false
Log File Parsing
# Parse Apache access logs
log_pattern = /^(?<ip>\S+) .+ \[(?<time>[^\]]+)\] "(?<method>\w+) (?<path>\S+) (?<version>[^"]+)" (?<status>\d+) (?<size>\d+)/
log = '192.168.1.1 - - [01/Jan/2024:12:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234'
match = log.match(log_pattern)
puts match[:ip] # "192.168.1.1"
puts match[:method] # "GET"
puts match[:status] # "200"
Data Cleaning
# Clean and normalize phone numbers
def clean_phone(phone)
# Remove all non-digits
cleaned = phone.gsub(/\D/, '')
# Format as (XXX) XXX-XXXX
if cleaned.length == 10
cleaned.gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3')
else
"Invalid phone number"
end
end
puts clean_phone("123-456-7890") # "(123) 456-7890"
puts clean_phone("(555) 123.4567") # "(555) 123-4567"
URL Parameter Extraction
# Extract parameters from RESTful URLs
route_pattern = /\/users\/(?<id>\d+)\/posts\/(?<post_id>\d+)/
url = "/users/123/posts/456"
params = url.match(route_pattern)
puts params[:id] # "123"
puts params[:post_id] # "456"
# Build a simple router
def route_match(path, pattern)
match = path.match(pattern)
match ? match.named_captures : nil
end
Performance Tips & Best Practices
⚡ Performance Considerations
- Compile once, use many: Store regex in constants or variables for reuse
- Use anchors:
^and$prevent unnecessary backtracking - Be specific:
[0-9]is faster than.when you need digits - Avoid nested quantifiers: Patterns like
(a+)+can cause exponential slowdown - Use non-capturing groups:
(?:pattern)when you don't need the match
🎯 Best Practices
- Test thoroughly: Regex can have edge cases; test with various inputs
- Document complex patterns: Use comments with
/xmodifier for readability - Validate user input: Always sanitize regex patterns from user input
- Consider alternatives: Sometimes string methods are simpler and faster
- Use tools: Regex testers like rubular.com help build and debug patterns
Common Gotchas and Solutions
⚠️ Watch Out For
Escaping Special Characters
Characters like . + * ? ^ $ { } [ ] \ | ( ) have special meaning in regex.
/\./ matches literal dots, /\$/ matches dollar signs
Greedy Quantifiers
.* matches as much as possible, which might not be what you want.
Use .*? for non-greedy matching
Case Sensitivity
Regex is case-sensitive by default. Remember the /i modifier for case-insensitive matching.
🚀 Next Steps
Now that you understand regular expressions, you're ready to tackle complex text processing tasks. Continue with:
- String Manipulation: Learn advanced string methods that work with regex
- Text Parsing: Parse structured data formats and extract information
- Data Validation: Build robust input validation for web applications
- File Processing: Process large text files and logs efficiently