Strings
Ruby’s String is a mutable, encoded sequence of bytes — interpreted as a sequence of characters in some encoding (UTF-8 by default). The principal forms are single-quoted (literal — no escape interpretation), double-quoted (interpolating, with escape sequences), heredoc (multi-line), and percent-literal (alternate delimiters). Strings are first-class objects with substantial methods: upcase, downcase, split, gsub, match, each_line, etc. The combination — mutable strings, encoding-aware operations, the conventional gsub and match for substantial text manipulation, string interpolation — covers the routine text-processing surface. The frozen_string_literal: true magic comment admits substantial efficiency through immutable literals.
String literals
Three principal forms:
'single quotes' # literal — no interpolation
"double quotes" # interpolation and escapes
'no\ninterpolation' # literal: \n is two characters
"newline\n and #{value}" # newline + interpolation
# Percent literals admit alternate delimiters:
%q(single-quoted equivalent)
%Q(double-quoted equivalent)
%(also double-quoted equivalent)
# Heredoc:
text = <<~END
This is a
multi-line
string.
END
The conventional contemporary discipline:
- Single quotes for literals without interpolation (substantially faster, clearer intent).
- Double quotes for interpolation and escape sequences.
- Heredoc for multi-line text.
frozen_string_literal: truemagic comment at the file top.
Interpolation
Double-quoted strings admit #{ expression }:
name = "Alice"
greeting = "Hello, #{name}!" # "Hello, Alice!"
age = 30
description = "#{name} is #{age} years old"
# Any expression:
result = "Sum: #{1 + 2}" # "Sum: 3"
formatted = "Pi: #{Math::PI.round(2)}" # "Pi: 3.14"
For complex interpolation, the format method (alias sprintf):
format("Pi is %.2f", Math::PI) # "Pi is 3.14"
format("%-10s | %5d", "alice", 30) # "alice | 30"
format("%s items at $%.2f", count, price)
Escape sequences
Double-quoted strings admit:
"\n" # newline
"\t" # tab
"\r" # carriage return
"\\" # backslash
"\"" # double quote
"\#{not interpolated}" # literal "#{...}"
"é" # Unicode é
"\u{1F600}" # 😀
"\x41" # "A"
Single-quoted strings only admit \\ and \':
'\\n' # 2 characters: \ and n
'\n' # 2 characters: \ and n
"\n" # 1 character: newline
Heredocs
Multi-line strings:
sql = <<~SQL
SELECT id, name
FROM users
WHERE active = true
SQL
# The ~ admits indented heredocs (Ruby 2.3+);
# the substantial whitespace is stripped from each line.
# Without ~, all leading whitespace is preserved:
sql = <<-SQL
SELECT id, name
FROM users
SQL
# Without - or ~, the closing must be at column 0:
sql = <<SQL
literal whitespace
SQL
# Single-quote-style (no interpolation):
text = <<~'TEXT'
No #{interpolation} here.
TEXT
The <<~ (squiggly heredoc) is the conventional contemporary form.
String methods
The substantial method surface:
s = "hello world"
s.length # 11
s.size # 11
s.empty? # false
s.include?("ll") # true
s.start_with?("hello") # true
s.end_with?("world") # true
s.index("o") # 4
s.count("l") # 3
s.upcase # "HELLO WORLD"
s.downcase # "hello world"
s.capitalize # "Hello world"
s.swapcase # "HELLO WORLD" → "hello world", "Hello" → "hELLO"
s.reverse # "dlrow olleh"
s.strip # remove leading/trailing whitespace
s.chomp # remove trailing newline
s.chop # remove last character
s.lstrip
s.rstrip
s.split(" ") # ["hello", "world"]
s.split # ["hello", "world"] (any whitespace)
s.split("") # ["h", "e", "l", ...]
s.chars # ["h", "e", "l", ...]
s.lines # ["hello world"] (lines with newlines)
s.each_char { |c| puts c }
s.each_line { |line| puts line }
s.gsub("l", "L") # "heLLo worLd"
s.gsub(/[aeiou]/, "_") # "h_ll_ w_rld"
s.sub("l", "L") # "heLlo world" (first only)
s.tr("aeiou", "*") # character substitution
s.delete("l") # "heo word"
s.squeeze # remove consecutive duplicates
s.center(20) # padding
s.ljust(20)
s.rjust(20)
s.ljust(20, "*") # custom pad
s.to_i # 0 (no integer prefix)
s.to_f # 0.0
s.to_sym # :"hello world"
s.bytes # array of byte values
s.encoding # #<Encoding:UTF-8>
Indexing and slicing
Three principal forms:
s = "hello world"
s[0] # "h"
s[-1] # "d"
s[100] # nil
s[0, 5] # "hello" (start, length)
s[6, 5] # "world"
s[0..4] # "hello" (range)
s[0...5] # "hello" (exclusive range)
s[6..] # "world" (endless)
s[/[a-z]+/] # "hello" (regex match)
s[/(.) (\w+)/, 2] # "world" (capture group)
The mechanism admits substantial flexibility for substring extraction.
Mutation
Strings are mutable; many methods have ! variants:
s = "hello"
s.upcase # "HELLO" (returns a new string)
puts s # "hello" (unchanged)
s.upcase! # mutates s in place
puts s # "HELLO"
s = "hello"
s << " world" # appends in place
s += " world" # creates a new string, reassigns
s.concat(" world") # appends in place
Under frozen_string_literal: true, string literals are frozen — mutation raises:
# frozen_string_literal: true
s = "hello"
s << " world" # raises FrozenError
# Defence: explicit unfreezing or new string:
s = "hello".dup
s << " world" # OK
The conventional contemporary discipline enables frozen_string_literal: true — admits substantial efficiency and prevents mutation bugs.
Regular expressions
Ruby has built-in regex literal syntax:
re = /[a-z]+/ # the literal form
re = Regexp.new("[a-z]+")
re = %r{/path/[a-z]+} # alternate delimiter
# Match:
"hello".match(re) # MatchData object
"hello" =~ re # 0 (index of first match) or nil
re.match?("hello") # true (Ruby 2.4+; doesn't capture)
# Named captures:
data = "Date: 2026-01-15"
m = data.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
m[:year] # "2026"
m[:month] # "01"
# Substitution:
"hello".gsub(/l/, "L") # "heLLo"
"hello".gsub(/(\w+)/) { |w| w.upcase } # "HELLO"
"abc 123".scan(/\d+/) # ["123"]
# Flags:
re = /hello/i # case-insensitive
re = /hello/m # . matches newline
re = /hello/x # extended (whitespace and comments allowed)
The conventional contemporary discipline uses match? for “does it match?” (no capture), match when captures are needed.
String formatting
Three principal forms:
printf-style
format("%s is %d", name, age) # "Alice is 30"
sprintf("%-10s | %5.2f", name, value) # left-padded
"%-10s | %5d" % [name, age] # the % operator
format("%05d", 42) # "00042"
format("%.3f", 3.14159) # "3.142"
format("%x", 255) # "ff"
format("%b", 10) # "1010"
Interpolation
"Hello, #{name}!"
"Value: #{value.round(2)}"
Format methods
"%05d" % 42 # "00042"
"%s is %d" % [name, age] # "Alice is 30"
Encoding
Each string carries an encoding:
s = "hello"
s.encoding # #<Encoding:UTF-8>
s.bytes # [104, 101, 108, 108, 111]
s.bytesize # 5
# UTF-8 strings:
s = "héllo"
s.length # 5 (characters)
s.bytesize # 6 (bytes; é is 2 bytes)
# Encoding conversion:
s.encode("ASCII-8BIT") # to binary
s.force_encoding("UTF-8") # reinterpret bytes as UTF-8
The conventional contemporary default is UTF-8; explicit encoding is needed only for binary protocols and legacy data.
Common patterns
Building a substantial string
# Inefficient (creates many intermediate strings):
s = ""
items.each { |x| s += x.to_s + "\n" }
# Efficient with mutation:
s = String.new
items.each { |x| s << x.to_s << "\n" }
# Or with join:
s = items.map(&:to_s).join("\n")
The join form is conventional for known collections; mutation with << is conventional for incremental building.
Multi-line strings
sql = <<~SQL
SELECT u.id, u.name, p.title
FROM users u
JOIN posts p ON p.user_id = u.id
WHERE u.active = TRUE
SQL
html = <<~HTML
<html>
<body>
<h1>#{title}</h1>
</body>
</html>
HTML
Parsing CSV-like content
"a,b,c".split(",") # ["a", "b", "c"]
"a,b,,c".split(",") # ["a", "b", "", "c"]
"a,b,,c".split(",", -1) # ["a", "b", "", "c"] (preserves trailing)
# Or use the CSV stdlib:
require "csv"
CSV.parse("a,b,c\n1,2,3", headers: true)
Validation with regex
def valid_email?(s)
s.match?(/\A[^@\s]+@[^@\s]+\z/)
end
def numeric?(s)
s.match?(/\A\d+\z/)
end
The \A and \z are start-and-end anchors that match only at the string boundaries (^ and $ match at line boundaries).
Replacing with a block
"hello world".gsub(/\w+/) { |w| w.capitalize } # "Hello World"
"abc 123".gsub(/\d+/) do |match|
(match.to_i * 2).to_s
end
# "abc 246"
Case-insensitive comparison
a.casecmp?(b) # true if same ignoring case
a.downcase == b.downcase # alternative
Character iteration
"héllo".each_char { |c| puts c } # h, é, l, l, o
"hello".chars # ["h", "e", "l", "l", "o"]
# Bytes:
"héllo".each_byte { |b| puts b } # 104, 195, 169, ...
"héllo".bytes # [104, 195, 169, 108, 108, 111]
Substring operations
s.start_with?("h", "g") # any of these?
s.end_with?("d", "n")
s.delete_prefix("hello") # if starts with, remove
s.delete_suffix("world") # Ruby 2.5+
Padding
"42".rjust(5, "0") # "00042"
"hello".ljust(10, "*") # "hello*****"
"hi".center(10, "-") # "----hi----"
inspect for debugging
s = "line\nwith\ttab"
puts s.inspect # "\"line\\nwith\\ttab\""
puts s # actual newline and tab
p s # equivalent to puts s.inspect
The inspect returns a developer-readable form; to_s returns the user-facing form.
Heredoc with interpolation control
# With interpolation (default):
greeting = <<~MSG
Hello, #{name}!
Today is #{Date.today}.
MSG
# Without interpolation (single-quote style):
template = <<~'TEMPLATE'
Hello, #{name}! # literal #{...}
Greeting placeholder.
TEMPLATE
A note on String vs Symbol
For repeated identifier-like values, symbols are conventionally preferred:
# Strings:
{ "name" => "Alice", "age" => 30 }
status = "active"
# Symbols:
{ name: "Alice", age: 30 }
status = :active
Symbols are interned (one object per name) and immutable; strings are unique and mutable. For hash keys, method names in metaprogramming, and small enumerated values, symbols are conventional. For user-facing text, strings are conventional.
A note on the conventional discipline
The contemporary Ruby strings advice:
- Use single quotes for literals without interpolation.
- Use double quotes for interpolation and escapes.
- Use heredocs (
<<~) for multi-line strings. - Use
frozen_string_literal: true— substantial efficiency. - Use
gsubwith regex for substitution. - Use
String#%orformatfor printf-style formatting. - Use
match?overmatchwhen captures are not needed. - Use
\Aand\zanchors for full-string regex matching. - Use
String.new(not"") for mutable strings under frozen-string-literal. - Use
inspectandpfor debugging.
The combination — multiple literal forms, substantial method surface, built-in regex literals, encoding awareness, the heredoc form for multi-line strings — is the substance of Ruby’s text-processing surface. The discipline produces concise, expressive string handling.