Loading
All articles
Programming 16 min read

Ruby String Methods: A Visual, Hands-On Guide

Learn Ruby's everyday string methods visually: indexes and slices, changing case, trimming spaces, bang methods and their gotchas, searching, sub vs. gsub, split and join, lining up text, converting, a real benchmark and exercises with answers.

nbkhope
On this page

Names, messages, passwords, web pages, chat logs, CSV files: a huge share of what programs handle is text. In Ruby, a piece of text is a String, and strings come with a remarkable toolbox of built-in methods for changing case, trimming spaces, searching, replacing, splitting and more.

In this guide you'll see how a string is laid out, tour the methods you'll use every day, learn the "bang" (!) methods and their sneaky gotchas, line up a receipt, and measure why one popular way of building strings gets dramatically slow. Exercises with answers wrap it up.

All the outputs below come from real runs with Ruby 3.3. The examples use p to print, because it shows strings in quotes, so you can see every space.

A string is a row of characters

A string is a row of characters, numbered from 0, or from -1 at the end. The word Rainbow has R at index 0 and w at index 6, which is also index -1. word[0, 4] starts at 0 and takes 4 characters, giving "Rain". word[4..] goes from index 4 to the end, giving "bow". word[0] is "R", word[-1] is "w", and an index past the end, like word[20], gives nil instead of an error

Each character has a numbered position, its index, counting from 0. Negative indexes count from the end. Square brackets pull out characters or whole slices:

1word = "Rainbow"
2p word[0]
3p word[3]
4p word[-1]
5p word[0, 4]
6p word[0..3]
7p word[4..]
8p word.length
9p word[20]
1"R"
2"n"
3"w"
4"Rain"
5"Rain"
6"bow"
77
8nil
  • word[0, 4] means "start at 0, take 4 characters".
  • word[0..3] means "from index 0 to index 3, inclusive".
  • word[4..] means "from 4 to the end".

So many methods

How many methods does a string have? Ruby will tell you:

1puts "A string answers to #{"Some String".methods.size} methods"
2puts "#{String.instance_methods(false).size} of them are defined by String itself"
3p "Some String".methods.sort.first(8)
4p "Some String".methods.grep(/case/).sort
1A string answers to 181 methods
2133 of them are defined by String itself
3[:!, :!=, :!~, :%, :*, :+, :+@, :-@]
4[:casecmp, :casecmp?, :downcase, :downcase!, :swapcase, :swapcase!, :upcase, :upcase!]

grep(/case/) filters the list down to methods with "case" in their name, a handy trick when you half-remember a method. Strings have more methods of their own than any other core class:

How many methods does each kind of Ruby object have? Methods each class defines itself, counted in Ruby 3.3.2: String 133, Array 108, Hash 72, Integer 65, Symbol 29. Counting the methods it inherits too, a string answers to 181 methods. Nobody memorizes them all: learn the twenty or so you'll use daily, and look up the rest

Don't worry, you won't need 181 of them. Here's a map of the ones you'll reach for most, grouped by job:

A map of everyday string methods. Change case: upcase, downcase, capitalize, swapcase. Trim and tidy: strip, lstrip and rstrip, squeeze, chomp. Ask questions: include?, start_with?, empty?, length. Find: index and rindex, count, scan, bracket slices. Replace: sub and gsub, tr, delete, reverse. Split and join: split, chars, lines, Array#join. Line up: ljust and rjust, center, format, star to repeat. Convert: to_i and to_f, to_sym, Integer(), to_s

Changing case

1p "Hello".upcase
2p "World".downcase
3p "How ARe You dOIng toDAY?".capitalize
4p "Hello World".swapcase
5p "ação e coração".upcase
1"HELLO"
2"world"
3"How are you doing today?"
4"hELLO wORLD"
5"AÇÃO E CORAÇÃO"

capitalize makes the first character uppercase and all the rest lowercase. And Ruby's case methods understand accented letters, not just A to Z.

Trimming spaces

Text typed by people (or copied from somewhere) often comes with extra spaces. Leading spaces are at the start; trailing spaces are at the end:

Trimming spaces: strip, lstrip and rstrip. The text "   , And then   " has three spaces on each side, shown as dots. text.lstrip removes the spaces on the left, giving ", And then   ". text.rstrip removes the ones on the right, giving "   , And then". text.strip removes both, giving ", And then". lstrip trims the left, leading side; rstrip the right, trailing side; and strip both. Perfect for cleaning up what users type
1padded = "   , And then   "
2p padded.lstrip
3p padded.rstrip
4p padded.strip
5p "  messy   spacing  ".squeeze(" ").strip
1", And then   "
2"   , And then"
3", And then"
4"messy spacing"

squeeze(" ") collapses runs of repeated spaces into one.

Bang methods: upcase vs. upcase!

None of the methods so far change the original string: they hand you a new one. Most of them have a twin ending in ! (pronounced "bang") that changes the string in place:

Without ! you get a new string; with ! the original changes. shout = sentence.upcase makes a brand-new string, I CLIMBED A TREE., and sentence still holds I climbed a tree. sentence.upcase! changes the same string in place, so sentence now holds I CLIMBED A TREE. Gotcha: a ! method returns nil when there was nothing to change, so "HELLO".upcase! gives nil
1sentence = "I climbed a tree."
2another_sentence = sentence.upcase
3
4puts "Original sentence: #{sentence}"
5puts "Another sentence:  #{another_sentence}"
6
7sentence.upcase!
8puts "Now sentence is:   #{sentence}"
Original sentence: I climbed a tree.
Another sentence:  I CLIMBED A TREE.
Now sentence is:   I CLIMBED A TREE.

The #{...} inside double quotes is string interpolation: Ruby runs the code inside the braces and drops the result into the text.

Gotcha 1: bang methods can return nil

When there's nothing to change, a bang method returns nil instead of the string:

shout = "HELLO"
p shout.upcase
p shout.upcase!
"HELLO"
nil

So never chain onto a bang method:

name = "ADA"
puts name.upcase!.length
1bang_chain.rb:2:in `<main>': undefined method `length' for nil (NoMethodError)
2
3puts name.upcase!.length
4                 ^^^^^^^

Gotcha 2: two names, one string

Assigning a string to a second variable doesn't copy it. Both names point to the same string, so changing it in place through one name changes it for the other too:

Two names, one string. original = +"Ruby", then same_string = original, then same_string << "!": both names, original and same_string, point to the same string, which is now "Ruby!". Assigning a string to another variable doesn't copy it: both names point to the same object, so changing it through one name changes it for both. Use .dup when you need a separate copy

That's the real reason to be careful with ! methods and <<: you might be changing a string that other parts of your program are still using. (You'll meet this in Exercise 1.)

Gotcha 3: frozen strings

Many Ruby files start with a magic comment that makes every string literal frozen, which means unchangeable:

1# frozen_string_literal: true
2
3greeting = "hello"
4greeting << " world"
frozen.rb:4:in `<main>': can't modify frozen String: "hello" (FrozenError)

Frozen strings are faster and safer, and future Ruby versions are moving toward freezing literals by default. When you need a string you can change, start from +"hello" or "hello".dup.

Building strings

1first_name = "Ada"
2last_name = "Lovelace"
3born = 1815
4
5p first_name + " " + last_name
6p "#{first_name} #{last_name} was born in #{born}"
7p "Ha" * 3
8p first_name + ", you are " + (2026 - born).to_s + " years old in 2026"
9greeting = +"Hello"
10greeting << ", " << first_name << "!"
11p greeting
12p format("%-10s|%6.2f", "Pi", 3.14159)
1"Ada Lovelace"
2"Ada Lovelace was born in 1815"
3"HaHaHa"
4"Ada, you are 211 years old in 2026"
5"Hello, Ada!"
6"Pi        |  3.14"
  • + joins strings, but both sides must be strings, hence .to_s on the number.
  • Interpolation "#{...}" converts automatically and usually reads best.
  • << (the "shovel") appends to an existing string, changing it in place.
  • format gives precise control: %-10s pads text to 10 characters, %6.2f shows a number with 2 decimals in 6 characters.

Why += gets slow

Building a big string piece by piece is common: a report, a CSV export, a web page. The innocent-looking report += piece has a hidden cost. Each time, Ruby builds a brand-new string containing everything so far, plus the new piece. We measured it:

Building a big string: += is quadratic, << is not. Gluing short lines together into one string, measured for this article with Ruby 3.3.2 on Windows, median of 5 runs, then the median of 3 program runs, on a logarithmic scale. 10,000 pieces: += took 0.34 seconds, << 0.6 milliseconds and join 0.2 milliseconds. 25,000 pieces: 1.2 seconds, 2.0 and 0.6 milliseconds. 50,000 pieces: 4.2 seconds, 4.7 and 1.5 milliseconds. 100,000 pieces: 16.1 seconds, 9.6 and 3.7 milliseconds. += makes a brand-new, ever longer string at every step, so the work grows with the square of the number of pieces. << adds to the same string, and join builds it in one go

With 100,000 pieces, += took 16 seconds, while << took under 10 milliseconds: about 1,700 times faster. Look at the pattern: doubling the pieces from 50,000 to 100,000 roughly quadrupled the += time (4.2 to 16.1 seconds), but only doubled the << time. That's what "quadratic" means, and it's why += feels fine in a quick test and then crawls on real data.

1report = ""
2pieces.each { |piece| report += piece }   # slow: a new string every time
3
4report = +""
5pieces.each { |piece| report << piece }   # fast: appends in place
6
7report = pieces.join                      # fast and simplest

Searching inside a string

1title = "Welcome to my world"
2p title.include?("world")
3p title.include?("earth")
4p title.start_with?("Welcome")
5p title.end_with?("!")
6p title.index("o")
7p title.rindex("o")
8p title.count("o")
9p title.index("earth")
1true
2false
3true
4false
54
615
73
8nil

Methods ending in ? answer yes-or-no questions with true or false. index gives the position of the first match (rindex, the last one), and nil when there isn't one.

Replacing: sub and gsub

sub changes the first match; gsub changes them all. Starting from I love pasta, pasta and more pasta, .sub("pasta", "pizza") gives I love pizza, pasta and more pasta, changing only the first, while .gsub("pasta", "pizza") gives I love pizza, pizza and more pizza, changing all three. The g in gsub stands for global. Both also accept patterns, regular expressions, like gsub(/\d/, "#") to hide every digit
1sentence = "I love pasta, pasta and more pasta"
2p sentence.sub("pasta", "pizza")
3p sentence.gsub("pasta", "pizza")
4p sentence
5p "2026-10-24".gsub("-", "/")
6p "Hello World".tr("lo", "01")
7p "Call 555-1234 or 555-9876".gsub(/\d/, "#")
8p "cat hat bat".gsub(/[ch]at/) { |word| word.upcase }
1"I love pizza, pasta and more pasta"
2"I love pizza, pizza and more pizza"
3"I love pasta, pasta and more pasta"
4"2026/10/24"
5"He001 W1r0d"
6"Call ###-#### or ###-####"
7"CAT HAT bat"
  • sub replaces the first match; gsub (global) replaces all of them. Neither changes the original; sub! and gsub! do.
  • tr swaps characters one for one: every l becomes 0 and every o becomes 1.
  • Both accept a regular expression (between slashes) to match patterns: /\d/ is "any digit", /[ch]at/ is "cat or hat".
  • With a block, gsub lets you compute each replacement.

Splitting and joining

split turns a string into an array; join turns it back. "red,green,blue".split(",") gives the array "red", "green", "blue", and .join(",") turns it back into one string. With no argument, split breaks on any whitespace: "the quick brown fox".split gives four words
1p "red,green,blue".split(",")
2p "the quick brown fox".split
3p "hello".chars
4p ["2026", "10", "24"].join("-")
5p "stressed".reverse
6p "one\ntwo\nthree".lines
7p "banana".chars.tally
1["red", "green", "blue"]
2["the", "quick", "brown", "fox"]
3["h", "e", "l", "l", "o"]
4"2026-10-24"
5"desserts"
6["one\n", "two\n", "three"]
7{"b"=>1, "a"=>3, "n"=>2}

Once a string is an array, every array method is at your fingertips. chars.tally counts how many times each letter appears.

Lining things up

ljust, rjust and center pad a string to a fixed width, which is perfect for tables and receipts in the terminal:

1items = { "Coffee" => 3.5, "Croissant" => 2.25, "Orange juice" => 4.0 }
2
3puts "RECEIPT".center(24, "=")
4items.each do |item, price|
5  puts item.ljust(16, ".") + format("%.2f", price).rjust(8)
6end
7puts "-" * 24
8puts "TOTAL".ljust(16) + format("%.2f", items.values.sum).rjust(8)
1========RECEIPT=========
2Coffee..........    3.50
3Croissant.......    2.25
4Orange juice....    4.00
5------------------------
6TOTAL               9.75

Converting to and from strings

Text that comes from users or files is always a string, even when it looks like a number:

1p "42".to_i
2p "3.14".to_f
3p "42abc".to_i
4p "abc".to_i
5p "ruby".to_sym
6p 42.to_s
7p 255.to_s(2)
8begin
9  Integer("42abc")
10rescue ArgumentError => error
11  puts "Integer() refused: #{error.message}"
12end
142
23.14
342
40
5:ruby
6"42"
7"11111111"
8Integer() refused: invalid value for Integer(): "42abc"

Empty, blank and length

1p "".empty?
2p "   ".empty?
3p "   ".strip.empty?
4p "abc".length
5p "é".length
6p "é".bytesize
1true
2false
3true
43
51
62

A string of spaces isn't empty, so strip first when checking for blank input. And length counts characters, while bytesize counts bytes: an accented letter is one character but two bytes in UTF-8.

Practice time

Try these before peeking at the answers.

Exercise 1: Predict the output (warm-up). What do the two puts lines print?

1original = +"Ruby"
2same_string = original
3same_string << "!"
4copy = original.upcase
5puts original
6puts copy

Exercise 2: Palindrome checker. Write palindrome?(text) that ignores case, spaces and punctuation, so "A man, a plan, a canal: Panama!" counts.

Exercise 3: Word frequency. Count how many times each word appears in "The cat and the hat and THE bat", ignoring case, and print a little bar chart, most common first.

Exercise 4: Title case. Turn "the lord of the RINGS" into "The Lord Of The Rings".

Exercise 5: Polite filter. Replace the words darn and heck (in any case) with asterisks of the same length.

Exercise 6: Caesar cipher (challenge). Shift every letter 3 places along the alphabet (a becomes d, z wraps around to c), keeping uppercase and lowercase. Then decode it again.

Quick quiz

  1. What's the difference between sub and gsub?
  2. What does "HELLO".upcase! return, and why?
  3. What do "Rainbow"[-1] and "Rainbow"[20] give?
  4. Why is "42abc".to_i risky for checking user input?
  5. Why is report += piece slow inside a big loop?

Answers

Exercise 1: Both variables point to the same string, so << through same_string also changed original. upcase (no bang) made a separate copy:

1original = +"Ruby"
2same_string = original
3same_string << "!"
4copy = original.upcase
5puts original
6puts copy
Ruby!
RUBY!

Exercise 2: Lowercase everything, delete anything that isn't a letter with a regular expression, then compare with the reverse:

1def palindrome?(text)
2  letters = text.downcase.gsub(/[^a-z]/, "")
3  letters == letters.reverse
4end
5
6["racecar", "A man, a plan, a canal: Panama!", "Ruby"].each do |text|
7  puts "#{text.inspect} -> #{palindrome?(text)}"
8end
"racecar" -> true
"A man, a plan, a canal: Panama!" -> true
"Ruby" -> false

Exercise 3: downcase, split, tally, then sort by count (and alphabetically for ties):

1text = "The cat and the hat and THE bat"
2counts = text.downcase.split.tally
3counts.sort_by { |word, count| [-count, word] }.each do |word, count|
4  puts "#{word.ljust(5)} #{'*' * count} #{count}"
5end
1the   *** 3
2and   ** 2
3bat   * 1
4cat   * 1
5hat   * 1

Exercise 4: Split into words, capitalize each, and join them back:

1def title_case(text)
2  text.split.map(&:capitalize).join(" ")
3end
4
5puts title_case("the lord of the RINGS")
The Lord Of The Rings

Exercise 5: A regular expression built from the word list: \b marks word boundaries, so "heckle" wouldn't be touched, and i ignores case. The block keeps the length:

1blocked_words = %w[darn heck]
2message = "Darn it, what the heck happened to my darn code?"
3
4clean = message.gsub(/\b(#{blocked_words.join('|')})\b/i) { |word| "*" * word.length }
5puts clean
**** it, what the **** happened to my **** code?

Exercise 6: tr can map a whole alphabet onto a shifted copy of itself in one go:

1def caesar(text, shift)
2  alphabet = ("a".."z").to_a.join
3  shifted = alphabet.chars.rotate(shift).join
4  text.tr(alphabet + alphabet.upcase, shifted + shifted.upcase)
5end
6
7secret = caesar("Hello, World!", 3)
8puts secret
9puts caesar(secret, -3)
Khoor, Zruog!
Hello, World!

Quiz: (1) sub replaces the first match; gsub replaces every match. (2) nil, because nothing needed changing: bang methods return nil when the string stays the same. (3) "w" (the last character) and nil (no character there). (4) It silently returns 42, and 0 for text with no digits, so bad input looks like a valid number; use Integer() to validate. (5) Every += creates a whole new string containing everything so far, so the copying grows with the square of the number of pieces; use << or join.

Cheat sheet

I want to…Write
Get a character / a slicetext[0], text[-1], text[0, 4], text[2..]
Change caseupcase, downcase, capitalize, swapcase
Trim spacesstrip, lstrip, rstrip, squeeze(" ")
Change the originalthe ! version: upcase!, strip!, gsub! (returns nil if unchanged)
Insert values"Hi, #{name}!"
Build a big stringreport << piece or pieces.join
Searchinclude?, start_with?, end_with?, index, count
Replace first / allsub("a", "b") / gsub("a", "b")
Split / joinsplit(","), chars, lines / array.join(", ")
Line up textljust(10), rjust(10), center(20, "="), format("%.2f", price)
Convertto_i, to_f, to_s, to_sym, Integer("42")
Check for blanktext.strip.empty?
Find a method"text".methods.grep(/case/)

Wrapping up

Strings are everywhere, and Ruby gives you a method for nearly anything you'd want to do with one:

  • Indexes start at 0, negative indexes count from the end, and [start, length] or ranges cut out slices.
  • Most methods return a new string. The ! versions change the original, return nil when nothing changed, and affect every variable pointing to that string.
  • sub replaces once, gsub everywhere, and both understand regular expressions.
  • split and join move between strings and arrays.
  • ljust, rjust, center and format line up text.
  • Build big strings with << or join, not +=: with 100,000 pieces, that was 16 seconds versus under 10 milliseconds in our test.

Want more Ruby? Read What Is the Ruby Programming Language?, then Ruby Arrays, Ruby Hashes and Basic File I/O in Ruby.

Looking for a reference text for Ruby? Get the Well-Grounded Rubyist.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.