Strings #

String is the data type you’ll encounter most often in nearly every Ruby program. From processing user input, building API responses, to reading text files — everything involves Strings. Ruby provides a very rich String standard library: dozens of built-in methods covering searching, transformation, formatting, encoding, and type conversion. Understanding these methods deeply — not just memorizing the syntax, but also knowing when and why to choose one method over another — is the foundation for writing idiomatic and efficient Ruby code.

Creating and Defining Strings #

Before diving into manipulation, it’s important to understand the various ways to define Strings in Ruby because each has different characteristics that affect program behavior.

Ruby provides several literal syntaxes for Strings:

# Single-quoted strings — doesn't interpret escape sequences
name = 'Umar ibn Khattab'
path = 'C:\Users\documents'       # backslashes aren't processed

# Double-quoted strings — interprets interpolation and escapes
greeting = "Assalamu'alaikum, #{name}"
new_line = "First line\nSecond line"

# Heredoc — for multi-line strings
text = <<~HEREDOC
  This is text
  spanning multiple lines
  with indentation cleaned automatically.
HEREDOC

# Frozen strings — immutable, more memory efficient
# frozen_string_literal: true
constant = "a value that never changes".freeze

The difference between single and double quotes isn’t just cosmetic. Single quotes are faster because Ruby doesn’t need to scan for interpolation characters, while double quotes are required when you need #{} or escape sequences like \n, \t.

flowchart TD
    A["String Definition"] --> B{"Need interpolation<br/>or escapes?"}

    B -- Yes --> C["Double quote<br/>&quot;Hello #123;name&#125;&quot;"]
    B -- No --> D["Single quote<br/>'Hello world'"]

    C --> E{"Need multi-line?"}
    D --> E

    E -- Yes --> F["Heredoc<br/>&lt;&lt;~TEXT<br/>...<br/>TEXT"]
    E -- No --> G["Use a regular literal"]

    G --> H{"Value must not<br/>change?"}

    H -- Yes --> I[".freeze"]
    H -- No --> J["Regular string"]

String Length and Checks #

Knowing the size and condition of a String is a basic operation often used for input validation or conditional logic.

text = "Bismillah"

# String length (in characters)
text.length    # => 9
text.size      # => 9  (alias of length)

# Length in bytes (differs with multi-byte characters)
text.bytesize  # => 9

# Condition checks
text.empty?           # => false
"".empty?             # => true
"  ".empty?           # => false  (spaces aren't empty!)

# Checking for nil or empty at once (needs ActiveSupport or a custom implementation)
# The idiomatic pure-Ruby way:
text.nil? || text.empty?   # => false

# Content checks
text.include?("llah")      # => true
text.start_with?("Bis")    # => true
text.end_with?("lah")      # => true
text.start_with?("Bis", "Al")  # => true  (can check several prefixes at once)

empty? only checks whether the string has zero length. A string containing only spaces — " " — isn’t considered empty. If you want to check for “meaningless” strings including whitespace-only ones, combine it with strip:

"   ".strip.empty?   # => true

Accessing and Slicing Strings #

Ruby allows access to specific parts of a String with very expressive syntax using indices or ranges.

sentence = "Alhamdulillah"

# Single character access (indices start at 0)
sentence[0]        # => "A"
sentence[-1]       # => "h"  (negative indices count from the end)
sentence[-3]       # => "l"

# Slicing with ranges
sentence[0..4]     # => "Alham"   (inclusive at both ends)
sentence[0...4]    # => "Alha"    (exclusive at the right end)
sentence[2, 5]     # => "hamdu"   (start at index 2, take 5 characters)

# Searching and fetching substrings
sentence["hamdu"]  # => "hamdu"   (nil if not found)
sentence["xyz"]    # => nil

# Fetching with regex
"price: 15000"[/\d+/]   # => "15000"

Slicing with [start, length] is very useful for binary protocols or parsing structured text formats. Note that indices exceeding the string length return nil, not an error — behavior you need to anticipate when parsing unpredictable data.

# ANTI-PATTERN: assuming indices are always valid
def fetch_code(str)
  str[0..2].upcase   # crashes if str is nil or too short
end

# CORRECT: validate before accessing
def fetch_code(str)
  return nil if str.nil? || str.length < 3
  str[0..2].upcase
end

Capitalization Transformations #

Ruby provides complete methods for changing String capitalization, from simple to Unicode-aware.

text = "hello world from ruby"

text.upcase       # => "HELLO WORLD FROM RUBY"
text.downcase     # => "hello world from ruby"
text.capitalize   # => "Hello world from ruby"   (first letter only)
text.swapcase     # => "HELLO WORLD FROM RUBY"   (flips all cases)

# For titles (capitalize each word) — no built-in method,
# but can be built with split + map
text.split.map(&:capitalize).join(" ")
# => "Hello World From Ruby"

All the methods above produce a new String. To modify the original string in place, use the bang versions:

name = "abu hurairah"
name.capitalize!
# name is now => "Abu hurairah"

# Bang versions return nil if there's no change
"ALREADY CAPITAL".upcase!   # => nil (no change)
Ruby’s built-in upcase and downcase weren’t always correct for non-ASCII characters in older Ruby versions. Since Ruby 2.4, these operations are Unicode-aware by default. Make sure you’re not stuck on the old behavior when working with multilingual text.

Searching and Pattern Checks #

Finding substrings or patterns within a String is a very common operation. Ruby provides several approaches with different trade-offs.

text = "Ruby is an elegant and expressive language"

# Position search
text.index("language")      # => 32   (first position, nil if absent)
text.rindex("a")            # => 41   (last position from the end)
text.index("not here")      # => nil

# Existence checks
text.include?("elegant")    # => true

# Regex matching
text.match?(/\belegant\b/)  # => true  (just true/false, faster)
text.match(/(\w+)\s+and/)    # => MatchData  (contains capture groups)

# Scan — fetch all matches
"I love ruby and ruby loves me".scan(/ruby/)
# => ["ruby", "ruby"]

"price: 100, quantity: 5".scan(/\d+/)
# => ["100", "5"]

Note the difference between match? and match:

MethodReturn ValueUse When
include?true/falseChecking a simple substring
match?true/falseChecking a regex pattern, need speed
matchMatchData / nilNeed capture groups from a regex
scanArrayFetch all occurrences of a pattern
indexInteger / nilNeed the character position
# Example of using match to extract data
email = "[email protected]"
result = email.match(/\A([\w+\-.]+)@([a-z\d\-.]+)\.([a-z]+)\z/i)

if result
  puts "Username: #{result[1]}"   # => "contact"
  puts "Domain: #{result[2]}"     # => "example"
  puts "TLD: #{result[3]}"        # => "com"
end

Replacement and Substitution #

After finding a pattern, the next step is often replacing it. Ruby distinguishes between replacing the first occurrence (sub) and all occurrences (gsub).

sentence = "the cat is cute, the cat is adorable"

# sub — replace only the FIRST occurrence
sentence.sub("cat", "dog")
# => "the dog is cute, the cat is adorable"

# gsub — replace ALL occurrences
sentence.gsub("cat", "rabbit")
# => "the rabbit is cute, the rabbit is adorable"

# With regex
"price: Rp 15.000".gsub(/[^0-9]/, "")
# => "15000"

# With a block — dynamic transformation
"hello world".gsub(/\b\w/) { |m| m.upcase }
# => "Hello World"

# With a hash — multiple replacements at once
"aeiou".gsub(/[aeiou]/, "a" => "@", "e" => "3", "o" => "0")
# => "@3i0u"
flowchart LR
    A[String Input] --> B{Replace how many\noccurrences?}
    B -- "First only" --> C[sub / sub!]
    B -- "All" --> D[gsub / gsub!]
    C --> E{Replacement\nsimple?}
    D --> E
    E -- "Yes, static string" --> F["gsub('old', 'new')"]
    E -- "Dynamic / transform" --> G["gsub(/pattern/) { |m| ... }"]
    E -- "Many at once" --> H["gsub(/pattern/, hash)"]
# ANTI-PATTERN: repeated gsub for many replacements
text = input
text = text.gsub("&", "&amp;")
text = text.gsub("<", "&lt;")
text = text.gsub(">", "&gt;")

# CORRECT: a single gsub with a hash
HTML_ESCAPES = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;" }
text = input.gsub(/[&<>]/, HTML_ESCAPES)

Cleaning and Trimming #

Real-world data often comes with unwanted whitespace, hidden characters, or padding that needs cleaning before processing.

dirty = "  \t Hello World \n  "

# strip — remove whitespace from both ends
dirty.strip    # => "Hello World"

# lstrip — remove whitespace from the left only
dirty.lstrip   # => "Hello World \n  "

# rstrip — remove whitespace from the right only
dirty.rstrip   # => "  \t Hello World"

# chomp — remove a trailing newline (very common for gets output)
"a line of text\n".chomp    # => "a line of text"
"a line of text\r\n".chomp  # => "a line of text"  (handles Windows line endings)

# chop — remove the LAST character whatever it is (rarely used)
"hello!".chop   # => "hello"

# delete — remove specific characters anywhere
"h-e-l-l-o".delete("-")     # => "hello"
"abc123".delete("0-9")       # => "abc"  (character ranges)

# squeeze — reduce repeated characters to one
"hellooooo".squeeze          # => "helo"
"  lots   of spaces  ".squeeze(" ")  # => " lots of spaces "

# tr — character transliteration (like sed y///)
"hello".tr("aeiou", "*")     # => "h*ll*"
"hello".tr("a-y", "b-z")     # => "ifmmp"  (shift one letter)

Combining strip with other operations is very common in input-processing pipelines:

def clean_input(raw)
  raw
    .strip
    .squeeze(" ")           # reduce multiple spaces
    .gsub(/[^\w\s\-]/, "")  # remove special characters except dashes
    .downcase
end

clean_input("  Hello   World!! \n")
# => "hello world"

Splitting and Joining #

Converting between Strings and Arrays is a very frequent operation, especially when processing CSV, tokens, or structured text.

# split — String to Array
"one,two,three".split(",")       # => ["one", "two", "three"]
"one  two   three".split         # => ["one", "two", "three"]  (default: whitespace)
"abcde".split("")                # => ["a", "b", "c", "d", "e"]  (per character)
"a,b,,c".split(",")              # => ["a", "b", "", "c"]
"a,b,,c".split(",", -1)          # => ["a", "b", "", "c"]  (-1: keep trailing empties)
"one,two,three".split(",", 2)    # => ["one", "two,three"]  (limit the number of pieces)

# join — Array to String (an Array method, not String)
["one", "two", "three"].join(", ")   # => "one, two, three"
["a", "b", "c"].join                 # => "abc"  (no separator)

# lines — split by lines
"line1\nline2\nline3".lines
# => ["line1\n", "line2\n", "line3"]

# chars — split per character
"hello".chars   # => ["h", "e", "l", "l", "o"]

# bytes — split per byte
"AB".bytes      # => [65, 66]
# Idiomatic pattern: split → map → join (transformation pipeline)
"name:email:phone"
  .split(":")
  .map { |field| field.capitalize }
  .join(" | ")
# => "Name | Email | Phone"

# ANTI-PATTERN: string concatenation in a loop
result = ""
["a", "b", "c"].each { |s| result += s }  # creates a new object each iteration

# CORRECT: use join
result = ["a", "b", "c"].join

Formatting and Padding #

Displaying data in a tidy format — CLI tables, structured logs, or readable output — requires formatting Strings with precision.

# center, ljust, rjust — padding with a fill character
"ruby".center(10)        # => "   ruby   "
"ruby".center(10, "-")   # => "---ruby---"
"ruby".ljust(10)         # => "ruby      "
"ruby".ljust(10, ".")    # => "ruby......"
"ruby".rjust(10)         # => "      ruby"
"ruby".rjust(10, "0")    # => "000000ruby"

# printf-style formatting with %
"Hello, %s! You are %d years old." % ["Umar", 30]
# => "Hello, Umar! You are 30 years old."

"Price: Rp %.2f" % 15000.5
# => "Price: Rp 15000.50"

# format / sprintf — more explicit
format("%-10s %5d", "apple", 100)
# => "apple        100"

# Example of a simple table output
header = format("%-15s %-10s %10s", "Name", "Position", "Salary")
row    = format("%-15s %-10s %10s", "Abu Bakar", "Manager", "Rp 15jt")
puts header
puts "-" * 38
puts row
SpecifierDescriptionExample
%sString"%s" % "hi""hi"
%dInteger"%d" % 42"42"
%fFloat"%.2f" % 3.14159"3.14"
%05dInteger with zero-padding"%05d" % 7"00007"
%-10sLeft-aligned string, width 10"%-10s" % "hi""hi "
%eScientific notation"%e" % 1234"1.234000e+03"
%xHexadecimal"%x" % 255"ff"

Type Conversion #

Strings often need converting to other types when processing user input or data from external sources. Ruby provides explicit, idiomatic conversion methods.

# String to numeric
"42".to_i          # => 42
"3.14".to_f        # => 3.14
"0xFF".to_i(16)    # => 255  (parse hexadecimal)
"0b1010".to_i(2)   # => 10   (parse binary)
"100".to_r         # => (100/1)  (Rational)
"3.14".to_c        # => (3.14+0i)  (Complex)

# Behavior with invalid strings
"abc".to_i         # => 0   (no error, but the result is 0!)
"3.14abc".to_f     # => 3.14  (stops at the first non-numeric character)
"abc".to_f         # => 0.0

# Integer() and Float() — stricter, raise exceptions if invalid
Integer("42")      # => 42
Integer("abc")     # => ArgumentError: invalid value for Integer
Float("3.14")      # => 3.14
Float("abc")       # => ArgumentError: invalid value for Float

# String to symbol and back
"method_name".to_sym    # => :method_name
:method_name.to_s       # => "method_name"

# String to an array of characters
"hello".chars           # => ["h", "e", "l", "l", "o"]

# Check whether a string is a valid number (Ruby has no built-in is_numeric)
def valid_number?(str)
  Integer(str) rescue false
end
# ANTI-PATTERN: using to_i for numeric validation
def process_age(input)
  age = input.to_i
  if age > 0  # WRONG: "abc".to_i == 0, not an error
    "Valid age: #{age}"
  else
    "Invalid input"
  end
end
# process_age("0") is wrongly considered invalid
# process_age("-5") passes without range validation

# CORRECT: use Integer() with rescue
def process_age(input)
  age = Integer(input)
  raise ArgumentError, "Age must be positive" unless age > 0
  "Valid age: #{age}"
rescue ArgumentError => e
  "Invalid input: #{e.message}"
end

Encoding and Unicode #

In the modern era, Ruby programs almost certainly deal with multilingual text. Understanding encoding is key to avoiding hard-to-trace bugs.

# Check the current string encoding
"hello".encoding        # => #<Encoding:UTF-8>

# Force encoding — relabel without converting data
str = "\xFF\xFE"
str.force_encoding("UTF-16LE")

# Encode — actual conversion (with data conversion)
"Héllo".encode("ISO-8859-1")   # => String in ISO-8859-1
"Héllo".encode("ASCII", invalid: :replace, undef: :replace, replace: "?")
# => "H?llo"  (undefined characters replaced with ?)

# Valid encoding?
"hello".valid_encoding?   # => true

# Character count vs bytes (important for multibyte)
arabic = "مرحبا"
arabic.length      # => 5  (5 characters)
arabic.bytesize    # => 10  (10 bytes in UTF-8, each Arabic character is 2 bytes)

# Iterate per character (correct for Unicode)
"こんにちは".each_char { |c| print c + " " }
# => こ ん に ち は
flowchart TD
    A[Incoming String] --> B{Is the encoding\nknown?}
    B -- Yes --> C{Encoding matches\nthe requirement?}
    B -- No --> D["Check .encoding\nor .valid_encoding?"]
    D --> C
    C -- Yes --> E[Use directly]
    C -- No --> F{Need data\nconversion?}
    F -- Yes --> G[".encode('target_encoding')"]
    F -- "Just relabel" --> H[".force_encoding('label')"]
    G --> I{Any invalid\ncharacters?}
    I -- Yes --> J["Use the\ninvalid: :replace option"]
    I -- No --> E
Never use force_encoding to “fix” strings with corrupted characters — this method only changes the encoding label without converting data. If you need a real conversion, use encode. This misconception is a very common source of encoding bugs.

String Iteration Methods #

Ruby allows iterating directly over a String without converting to an Array first, which is more memory-efficient for large strings.

text = "First line\nSecond line\nThird line"

# each_line — iterate per line
text.each_line do |line|
  puts line.chomp.upcase
end
# FIRST LINE
# SECOND LINE
# THIRD LINE

# each_char — iterate per character
counts = Hash.new(0)
"mississippi".each_char { |c| counts[c] += 1 }
# => {"m"=>1, "i"=>4, "s"=>4, "p"=>2}

# each_byte — iterate per byte (for binary protocols)
"ABC".each_byte { |b| print "#{b} " }
# => 65 66 67

# upto — iterate strings in lexicographic order
"a".upto("e") { |c| print c + " " }
# => a b c d e

"aa".upto("ac") { |s| print s + " " }
# => aa ab ac

Other Utility Methods #

There are some String methods that don’t fit the categories above but are very useful in daily practice.

# reverse — reverse the string
"hello".reverse      # => "olleh"

# count — count character occurrences
"mississippi".count("s")    # => 4
"hello world".count("aeiou")  # => 3

# sum — sum the ASCII values
"ABC".sum      # => 198  (65 + 66 + 67)

# hex, oct — parse a string as a hex/octal number
"ff".hex       # => 255
"77".oct       # => 63

# succ / next — increment the string
"a".succ       # => "b"
"z".succ       # => "aa"
"az".succ      # => "ba"
"zz9".succ     # => "aaa0"

# Useful for generating simple IDs or sequences
code = "A0"
5.times { puts code = code.succ }
# A1, A2, A3, A4, A5

# replace — replace content in place (mutates the same string)
str = "hello"
str.replace("world")
# str is now => "world"  (the same object, not a new one)

# dup and clone — copy strings
original = "hello".freeze
copy = original.dup    # can be mutated
copy << " world"       # => "hello world"

When to Switch to a Different Approach #

Although Ruby’s String standard library is very complete, there are situations where you need different tools:

Keep using built-in Strings when:
  ✓ Simple to medium text manipulation
  ✓ Searching and substituting with substrings or simple regexes
  ✓ Formatting output for CLI or logs
  ✓ Parsing not-too-complex text formats
  ✓ Performance is sufficient for normal data volumes

Consider alternative approaches when:
  ✗ Parsing HTML/XML — use Nokogiri (don't parse with regex!)
  ✗ Parsing complex CSV — use the csv library from stdlib
  ✗ Parsing JSON — use the built-in JSON.parse
  ✗ Complex text templates — use ERB or Mustache
  ✗ Very large text manipulation — consider StringIO for streaming
  ✗ Very complex pattern matching — break it into a structured parser

Summary #

  • Choose the right literal syntax — single quotes for static strings, double quotes for interpolation and escape sequences, heredocs for multi-line text.
  • strip vs chompstrip cleans all whitespace at both ends, chomp only removes a trailing newline; they have different use cases.
  • sub vs gsubsub replaces the first occurrence, gsub replaces all; use blocks or hashes for dynamic transformations and multiple replacements.
  • to_i isn’t safe for validation — use Integer() and Float(), which raise exceptions when the input is invalid.
  • match? is faster than match — use match? when you only need true/false, save match for when you need capture groups.
  • Character count vs bytes — for Unicode/multibyte strings, length gives the character count, bytesize gives the byte count; they differ for non-ASCII text.
  • force_encoding isn’t for fixing — it only relabels the encoding without converting data; use encode for real conversion.
  • The split → map → join pipeline is an idiomatic Ruby pattern for structured text transformation that’s easy to read and maintain.
  • Avoid string concatenation in loops — use join or << (the shovel operator) instead of +=, which creates a new object every iteration.

← Previous: Articles & Resources   Next: IO →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact