rguides

String Manipulation with stringr

String manipulation is a fundamental skill for any R programmer. Whether you’re cleaning messy data, parsing text files, or building features from textual data, the stringr package provides a consistent, readable interface for all string operations. Part of the Tidyverse, stringr wraps base R’s inconsistent string functions with a unified API.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with String Manipulation with stringr. By the end, you will know how to apply the core functions in real data analysis workflows.

Installing and loading stringr

If you’ve installed the Tidyverse, stringr is already available. Otherwise, install it separately:

install.packages("stringr")
library(stringr)

All stringr functions start with str_, making them easy to discover with autocompletion. This prefix convention means typing str_ in RStudio shows every available stringr function in the autocomplete dropdown, which is far more discoverable than memorizing base R’s sub(), gsub(), nchar(), substr(), and paste() with their inconsistent naming patterns.

Basic string operations

Measuring string length

The str_length() function returns the number of characters in each string, handling multi-byte UTF-8 characters and NA values correctly. Unlike base R’s nchar(), which requires a separate type argument for byte vs character counts, str_length() always reports the character count and returns NA for NA inputs rather than 2 (the string “NA”):

words <- c("hello", "world", "R programming")

str_length(words)
# [1] 5 5 14

This is equivalent to nchar() but handles NA more gracefully by default. The length measurement includes spaces and punctuation, and for empty strings str_length() returns 0 rather than NA, which makes it safe to use inside filtering pipelines without special-case handling.

Extracting substrings

str_sub() extracts or replaces portions of a string by character position. Unlike base R’s substr(), it handles negative indices correctly (counting from the end of the string), and it can be used on the left side of an assignment to modify the string in place without creating a copy of the entire vector:

text <- "R programming is fun"

# Extract characters from position 1 to 3
str_sub(text, 1, 3)
# [1] "R p"

# Replace substring in place
str_sub(text, 1, 3) <- "Py"
text
# [1] "Py programming is fun"

Combining strings

str_c() concatenates strings element-wise, like paste0() but with stricter NA handling. If any input is NA, the output for that element is NA rather than the literal text “NA” — a common source of subtle bugs when using base R’s paste(). The sep argument controls what goes between paired elements, while collapse turns the entire result into a single string:

first_name <- "John"
last_name <- "Doe"

str_c(first_name, " ", last_name)
# [1] "John Doe"

# For vectors, use sep and collapse
str_c(c("a", "b", "c"), 1:3, sep = "-")
# [1] "a-1" "b-2" "c-3"

str_c(c("a", "b", "c"), collapse = ", ")
# [1] "a, b, c"

Pattern matching

Stringr’s pattern matching functions accept regular expressions by default, making them powerful tools for searching, extracting, and replacing text. Every stringr function that takes a pattern can also use fixed strings (via fixed()) or boundary-aware matching (via boundary()), giving you full control over the matching strategy.

Detecting patterns

str_detect() returns TRUE where a pattern exists in each string element. This logical vector is especially useful inside dplyr::filter() for selecting rows that match a text criterion. You can also sum the result to count how many strings contain the pattern:

fruits <- c("apple", "banana", "cherry", "date")

# Find strings containing 'a'
str_detect(fruits, "a")
# [1] TRUE TRUE TRUE FALSE

Extracting matches

Extract the actual matched text with str_extract(). Unlike str_detect() which only answers “does it match?”, str_extract() returns the matching substring itself. For extracting structured data like email domains or dates from free text, str_extract() is more useful than detection because you get the value you want rather than just a yes/no answer:

emails <- c("user@domain.com", "test@example.org", "invalid")

str_extract(emails, "@\\w+\\.\\w+")
# [1] "@domain.com" "@example.org" NA

Replacing patterns

Replace matched patterns with str_replace(). This function substitutes only the first occurrence of the pattern in each string. Use str_replace_all() when you need to replace every instance — for example, when standardizing terminology across a corpus, removing all punctuation, or normalizing abbreviations that appear multiple times in the same text:

text <- "The cat sat on the mat"

str_replace(text, "cat", "dog")
# [1] "The dog sat on the mat"

# Replace all matches
str_replace_all(text, "at", "ot")
# [1] "The cot sot on the mot"

Splitting strings

str_split() breaks a string into pieces at each occurrence of a delimiter and returns the result as a list. This preserves the grouping of pieces per original string, unlike flatten()-based approaches that lose track of which pieces belong together. For fixed-width structured data, str_split_fixed() returns a matrix with a guaranteed number of columns:

sentence <- "one,two,three,four"

str_split(sentence, ",")
# [[1]]
# [1] "one"   "two"   "three" "four"

Working with whitespace

Trimming whitespace

Remove leading and trailing whitespace with str_trim(). Extra whitespace at the edges of strings is common when importing data from spreadsheets, web scraping, or merging data from multiple sources, and it causes failures in joins, filter conditions, and string equality comparisons that are hard to spot visually:

messy <- "   clean this   "

str_trim(messy)
# [1] "clean this"

Squishing whitespace

str_squish() collapses multiple internal whitespace characters into single spaces while also trimming the edges. It is essentially str_trim() plus str_replace_all(x, \"\\\\s+\", \" \") in one call. This is the go-to function for normalizing free-text survey responses where respondents used inconsistent spacing:

ugly <- "too    many     spaces"

str_squish(ugly)
# [1] "too many spaces"

Padding strings

str_pad() adds characters to the left, right, or both sides of strings to reach a consistent width. This is essential for generating fixed-width file formats, formatting identifiers with leading zeros, or aligning columns in plain-text console output. The pad argument accepts any single character, and negative widths are treated as zero:

numbers <- c("1", "25", "300")

str_pad(numbers, width = 5, side = "left", pad = "0")
# [1] "00001" "00025" "00300"

Practical examples

Validating email addresses

Combine stringr functions for data validation. The pattern ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ checks that an email string starts with allowed characters, contains a single @, and ends with a dot followed by two or more letters. This regex catches obvious malformations but is not RFC-compliant — for production email validation, send a confirmation message rather than relying solely on regex:

is_valid_email <- function(email) {
  pattern <- "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
  str_detect(email, pattern)
}

emails <- c("user@domain.com", "invalid@", "test@site.org")
is_valid_email(emails)
# [1] TRUE FALSE TRUE

Extracting numbers from text

Pull numeric values from mixed text by stripping non-numeric characters. The pattern \\$ targets the dollar sign, and str_replace_all() removes every occurrence before as.numeric() converts the cleaned string to a number. For more complex extractions (negative numbers, decimal points, comma separators), use str_extract() with a pattern like [-]?[0-9,.]+ to capture the numeric portion without removing it:

prices <- c("$19.99", "$25.50", "$9.99")

# Remove $ sign, convert to numeric
as.numeric(str_replace_all(prices, "\\$", ""))
# [1] 19.99 25.50  9.99

Cleaning names

Standardize names with consistent formatting by chaining stringr functions in a pipeline. str_squish() removes stray whitespace, and str_to_title() capitalizes the first letter of each word. This pipeline pattern is reusable across any text normalization task: trim whitespace, normalize case, then apply domain-specific transformations like removing titles or standardizing suffixes:

names <- c("  john doe ", "JANE SMITH", "Alice Bob")

names |>
  str_squish() |>
  str_to_title()
# [1] "John Doe"    "Jane Smith"  "Alice Bob"

Extracting substrings

str_extract(x, pattern) returns the first match per string, or NA if no match. str_extract_all(x, pattern) returns all matches as a list. str_match(x, pattern) with capturing groups returns a matrix with the full match and each captured group in separate columns, the key tool when you need to extract parts of a structured string.

Replacing and transforming

str_replace(x, pattern, replacement) replaces the first match. str_replace_all(x, pattern, replacement) replaces all. str_remove(x, pattern) removes the first match (equivalent to replacing with an empty string). str_to_lower(), str_to_upper(), and str_to_title() change case. str_to_sentence() capitalizes only the first character.

Splitting and combining

str_split(x, pattern) splits on a pattern, returning a list. str_split_fixed(x, pattern, n) returns a matrix with exactly n columns, safer when you know the structure. str_c(x, y, sep = " ") concatenates corresponding elements. str_flatten(x, collapse = ", ") collapses a vector to a single string with a separator.

String padding and formatting

str_pad(x, width, side = "left", pad = "0") pads strings to a minimum width. str_trunc(x, width) truncates long strings with an ellipsis. str_wrap(x, width = 80) wraps long strings at word boundaries, useful for plot labels. str_glue("Hello, {name}!") substitutes variable values from the current environment, like Python f-strings.

Pattern matching and detection

The core of stringr is pattern matching via regular expressions. str_detect(x, pattern) returns a logical vector indicating which strings match. str_which(x, pattern) returns the integer indices of matches. str_count(x, pattern) counts non-overlapping occurrences of the pattern in each string.

For extracting matched text: str_extract(x, pattern) returns the first match in each string (or NA if no match), and str_extract_all(x, pattern) returns all matches as a list. str_match(x, pattern) returns a matrix where the first column is the full match and subsequent columns are capture groups. str_match_all() returns a list of matrices for multiple matches.

str_locate(x, pattern) returns the start and end positions of the first match. This is useful when you need to know where in the string the match occurred, not just what matched. str_sub() can then extract or replace by position.

String splitting and joining

str_split(x, pattern) splits each string into a list of pieces. str_split_fixed(x, pattern, n) returns a character matrix with exactly n columns, making the output easier to work with in a data frame. The n parameter controls the maximum number of splits: str_split_fixed(x, "_", 2) splits on the first underscore and puts the rest in the second column.

str_c(x, y, sep = "") concatenates strings element-wise, like paste0() but with consistent NA handling (if any input is NA, the output is NA, not the string “NA”). str_flatten(x, collapse = ", ") collapses a vector to a single string, equivalent to paste(x, collapse = ", ").

For padding and trimming: str_pad(x, width, side = "left", pad = " ") pads a string to a minimum width; str_trim(x) removes leading and trailing whitespace; str_squish(x) also collapses internal whitespace to single spaces.

Case conversion and string manipulation

str_to_upper(), str_to_lower(), str_to_title(), and str_to_sentence() handle case conversion. These functions are locale-aware, str_to_upper("i", locale = "tr") produces the Turkish dotted capital I rather than the ASCII I. For locale-insensitive operations on ASCII text, the base R toupper() and tolower() are safe.

str_replace(x, pattern, replacement) replaces the first match; str_replace_all(x, pattern, replacement) replaces all matches. The replacement string can include back-references to capture groups using \1, \2, etc. For fixed string replacement without regex interpretation, wrap the pattern in fixed(): str_replace_all(x, fixed("."), "_").

str_remove(x, pattern) is shorthand for str_replace(x, pattern, ""), and str_remove_all() removes all matches. These are convenient for stripping unwanted characters from strings.

Boundary detection and encoding

str_length(x) returns the number of characters (Unicode code points), not bytes. For multi-byte UTF-8 strings, this differs from nchar(x, type = "bytes"). str_sub(x, 1, 3) extracts characters 1 through 3 by position; negative indices count from the end, so str_sub(x, -3, -1) gives the last three characters.

Boundary detection uses the boundary() function: str_split(x, boundary("word")) splits on word boundaries rather than a literal pattern. This correctly handles punctuation attached to words. boundary("sentence") and boundary("line_break") work similarly.

For working with non-ASCII text, stringr delegates to the ICU library through the stringi package. str_conv(x, encoding) converts encodings. When reading files with non-UTF-8 text, read with the correct encoding first: readr::read_csv("file.csv", locale = locale(encoding = "latin1")), then work with the result in UTF-8.

Practical patterns

A common data-cleaning task is normalizing free-text fields from survey responses. The pipeline typically looks like: str_squish() to remove extra whitespace, str_to_lower() to normalize case, str_remove_all() to strip punctuation, and str_replace_all() with a lookup vector to standardize synonyms. Passing a named character vector to str_replace_all() applies multiple replacements in one call: str_replace_all(x, c("colour" = "color", "favour" = "favor")).

For email validation, a minimal check is str_detect(email, "^[^@]+@[^@]+\.[^@]+$"), not exhaustive but catches obvious malformations. For URL extraction from text, str_extract_all(text, "https?://[\w./%-]+") handles most cases. For phone number normalization, strip everything except digits first with str_remove_all(phone, "[^0-9]") and then validate length.

stringr’s design

stringr is a consistent interface to string operations in R. Base R string functions, substr, sub, gsub, regexpr, regmatches, share no common naming conventions and have inconsistent argument orders. stringr fixes both problems: all functions start with str_, all take the string vector as the first argument (enabling pipe usage), and all return the same type for the same input type. Learning the prefix makes functions discoverable; the consistent first argument makes pipelines natural.

Under the hood, stringr uses the ICU regular expression engine through the stringi package. ICU provides Unicode-aware matching that handles international characters, locale-specific collation, and properties like letter or digit that span all scripts. If you need more of ICU’s capabilities than stringr exposes, stringi provides direct access to the full API.

Fixed strings vs patterns

Many stringr functions accept both fixed strings and regular expression patterns. By default, the pattern argument is interpreted as a regex. Wrap a fixed string in fixed() to tell stringr to match it literally, which is faster and avoids escaping metacharacters. Use regex() with perl = TRUE to enable Perl-compatible regex features. Use coll() for locale-aware string comparison that handles language-specific sorting and equivalence rules.

Choosing between fixed and regex matching is a practical decision. If the string to match contains special regex characters — dots, parentheses, asterisks, square brackets — either escape them with two backslashes or use fixed(). Forgetting to escape is the most common stringr bug: “1.5” as a pattern matches “1x5” because the dot matches any character.

Vectorization over strings and patterns

stringr functions are vectorized over both the string argument and the pattern argument. Passing a vector of patterns to str_detect returns a matrix of logical values — one row per string, one column per pattern. This matrix approach replaces a for loop over patterns. For checking whether a string matches any of several patterns, collapse the results with rowAny or use str_detect with a single regex alternation.

Processing many strings simultaneously without explicit loops is one of R’s strengths. A common mistake is writing a for loop to apply string operations to each row of a data frame when vectorized string functions already operate on the whole column at once. Profile string-heavy code if it runs slowly — replacing a loop over a character vector with a vectorized stringr call often produces a large speedup.

Summary

The stringr package provides consistent, readable functions for string manipulation:

FunctionPurpose
str_length()Count characters
str_sub()Extract/replace substrings
str_c()Concatenate strings
str_detect()Find pattern matches
str_extract()Pull matched text
str_replace()Substitute patterns
str_trim()Remove outer whitespace
str_squish()Collapse internal whitespace
str_pad()Add padding characters

These functions form the foundation for text processing in R. Combined with regular expressions, stringr handles virtually any string manipulation task you’ll encounter.

Next steps

Now that you understand string manipulation with stringr, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.

See also