Polyglot
Languages Ruby strings
Ruby § strings

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: true magic 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 gsub with regex for substitution.
  • Use String#% or format for printf-style formatting.
  • Use match? over match when captures are not needed.
  • Use \A and \z anchors for full-string regex matching.
  • Use String.new (not "") for mutable strings under frozen-string-literal.
  • Use inspect and p for 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.