R PROGRAMMING • TEXT AND DATES

Basic String Functions — Use basic string functions (paste/paste0, substr, nchar, grep/grepl) (intro)

Master R's core string manipulation toolkit for concatenating, extracting, measuring, and searching text data.

Historical Context & Motivation

String manipulation is one of the most pervasive tasks in computing, from parsing log files and cleaning survey data to constructing SQL queries dynamically. When R was created in the mid-1990s by Ross Ihaka and Robert Gentleman at the University of Auckland, the language inherited many of its text-handling idioms from S, the statistical language developed at Bell Labs. S itself drew on Unix shell utilities like grep, sed, and awk that had been the backbone of text processing on Unix systems since the 1970s. Understanding this lineage clarifies why R's string functions carry names and conventions that may feel different from languages like Python or Java, yet are deeply rooted in a tradition of expressive, vectorized text operations.

1973
grep Arrives on Unix
Ken Thompson writes the original grep utility for Unix, implementing regular-expression searching on streams of text. The name stands for "globally search for a regular expression and print matching lines." This tool becomes foundational for all later pattern-matching APIs.
1988
S Language Text Functions
The S language at Bell Labs introduces vectorized string functions such as paste, nchar, and substring, enabling analysts to operate on entire character vectors rather than looping element-by-element.
1995
R Inherits S's String Toolkit
Ihaka and Gentleman release R as an open-source implementation of S. All core string functions—paste, substr, nchar, grep, grepl—are part of base R from the outset.
2005–2010
paste0 and Tidyverse Wrappers
R 2.15.0 adds paste0 as a convenience shortcut for paste(..., sep = ""). Meanwhile, the stringr package begins providing consistent, tidyverse-friendly wrappers around these base functions.

Despite the emergence of packages like stringr and stringi, the base-R string functions remain essential for several reasons: they require no additional package dependencies, they appear ubiquitously in legacy codebases and CRAN packages, and they are the primitives on which higher-level wrappers are built. The central question this lesson addresses is straightforward yet fundamental: how do you concatenate, measure, extract, and search character vectors in R using only the built-in toolkit?

Core Principles & Definitions

Before diving into individual functions, it is essential to grasp the design philosophy that unifies R's string operations. Unlike languages where strings are objects with methods, R treats strings as atomic vectors of type character. Every string function in base R is vectorized: given a character vector of length n, it applies the operation element-wise and returns a result of length n (or a length determined by recycling rules). This vectorized behavior eliminates explicit loops and is the first principle you should internalize.

1

Concatenation: paste / paste0

Combine multiple strings (or vectors) into one. paste() inserts a separator (default " ") between arguments; paste0() is shorthand for sep = "". The collapse argument reduces a vector to a single string.
2

Measurement: nchar

Returns the number of characters in each element of a character vector. Comparable to len() in Python or .length() in Java, but applied element-wise to an entire vector. Beware: nchar(NA) returns NA by default.
3

Extraction: substr

Extracts (or replaces) a substring from position start to position stop inclusive. R uses 1-based indexing, so the first character is at position 1, not 0.
4

Pattern Matching: grep / grepl

grep(pattern, x) returns the indices of elements matching a pattern (or the values themselves with value = TRUE). grepl() returns a logical vector of the same length as x. Both accept regular expressions by default.
5

Vectorization Everywhere

All five functions operate element-wise on character vectors. When arguments differ in length, R applies standard recycling rules, silently repeating the shorter vector. This enables compact, loop-free code but demands attention to vector alignment.
KEY TAKEAWAY
Think of R's string functions like assembly-line robots in a factory. Each robot performs one operation—measuring, cutting, gluing, or inspecting—but it does so on every item passing along the conveyor belt (the vector) simultaneously. You do not need to hand each item to the robot individually; the vectorization handles that. The paste robot glues parts together, nchar measures each item's length, substr trims items to size, and grep/grepl acts as quality control, flagging items that match a specification.

Visual Explanation — Function Signatures & Data Flow

The following diagram maps out the five core string functions along with their primary arguments and return types. Each function takes a character vector (or multiple vectors) as input and produces either a character vector, an integer vector, or a logical/integer vector as output. Studying this map will help you quickly identify which function to reach for in any given text-processing scenario.

The top row shows each function's signature and return type. The bottom rows trace a character vector through successive transformations: paste collapses the vector, nchar measures it, and substr extracts a slice.

Notice the consistent pattern: every function accepts a character vector and returns a vector of the same length (or a collapsed scalar when collapse is specified). The return type varies—character for paste/paste0/substr, integer for nchar and grep, and logical for grepl—but the vectorized paradigm remains constant. This predictability is what makes base-R string functions composable: you can chain them in pipelines or nest them freely.

How Each Function Works — Detailed Mechanism

paste() and paste0() — String Concatenation

The paste function accepts an arbitrary number of arguments (which are coerced to character if needed), concatenates them element-wise with the string specified in sep (default " "), and optionally collapses the resulting vector into a single string via the collapse argument. Formally, if you supply vectors v₁, v₂, …, vₖ, R first recycles them to a common length n, then for each index i in 1…n it produces v₁[i] <sep> v₂[i] <sep> … <sep> vₖ[i]. If collapse is non-NULL, the n intermediate strings are then concatenated into a single scalar with the collapse string as a delimiter.

PASTE SEMANTICS
paste(v₁, v₂, sep = s)[i] = v₁[i] ‖ s ‖ v₂[i] for i ∈ {1, …, n}
Here ‖ denotes string concatenation, s is the separator, and n is the recycled common length. paste0(v₁, v₂) is equivalent to paste(v₁, v₂, sep = "").

nchar() — Character Counting

The nchar function counts the number of characters (or bytes, depending on the type argument) in each element of a character vector. By default type = "chars" counts Unicode characters, which is appropriate for most natural-language text. Setting type = "bytes" counts raw bytes, which differs for multi-byte encodings like UTF-8. A common pitfall is confusing nchar(x) with length(x): the former counts characters within each string, while the latter counts the number of elements in the vector.

NCHAR VS LENGTH
nchar(c("hello", "hi")) → c(5, 2) but length(c("hello", "hi")) → 2
nchar is element-wise (returns a vector of the same length), whereas length reports the number of elements in the vector itself.

substr() — Positional Extraction

The substr function takes three arguments: a character vector x, an integer start, and an integer stop. It returns the substring from position start to position stop inclusive, using 1-based indexing. If stop exceeds the string length, R silently truncates to the available characters rather than raising an error. Notably, substr can also perform replacement assignment: substr(x, 2, 3) <- "XX" modifies characters 2 and 3 in place.

grep() and grepl() — Pattern Searching

Both grep and grepl search a character vector for elements matching a pattern, which by default is interpreted as a POSIX-extended regular expression. The critical difference lies in the return type: grep returns an integer vector of matching indices (or the matching values if value = TRUE), while grepl returns a logical vector of the same length as the input. This distinction determines how you use the result: grep is ideal for subsetting by position, while grepl integrates seamlessly with logical indexing and dplyr::filter(). Setting fixed = TRUE disables regex interpretation, which is both safer and faster when you need literal matching.

GREP VS GREPL RETURN TYPES
grep("err", log_vec) → c(2, 5, 7) grepl("err", log_vec) → c(FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, TRUE)
Given a vector log_vec of 7 log messages, grep returns the positions of matches, whereas grepl returns a logical mask. Use log_vec[grepl("err", log_vec)] or log_vec[grep("err", log_vec)] to extract the matching elements.

Detailed Comparison — paste vs paste0, grep vs grepl

A frequent source of confusion for newcomers is deciding between paste and paste0, or between grep and grepl. The diagram below visualizes these pairs side by side, and the comparison table that follows codifies the differences systematically.

Left panels show paste (with configurable sep) and grep (returning indices). Right panels show their counterparts paste0 (no separator) and grepl (returning logicals).
Quick-reference comparison of the five base-R string functions
Attributepaste()paste0()grep()grepl()
Default sep" """ (empty)N/AN/A
collapse argYesYesN/AN/A
Return typecharactercharacterinteger (or character)logical
Regex supportNoNoYes (default)Yes (default)
Primary use caseBuild labels, messagesBuild file paths, IDsFind matching positionsFilter / boolean mask

Worked Example — Cleaning and Searching Log Data

Suppose you have a character vector representing server log entries and you need to (1) build a standardized file name, (2) count the length of each entry, (3) extract a date prefix, and (4) filter for entries containing the word "ERROR". This example walks through each task using the five functions covered in this lesson.

Processing Server Logs with Base-R String Functions
1
Step 1 — Define the DataCreate a character vector of log entries and a base directory path: logs <- c("2025-01-10 INFO startup complete", "2025-01-10 ERROR disk full", "2025-01-11 WARN memory high", "2025-01-11 ERROR timeout") base_dir <- "/var/log/app"
2
Step 2 — Build a File Path with paste0Concatenate the base directory, a slash, and a file name without any separator: file_path <- paste0(base_dir, "/server.log") Because paste0 uses no separator, the result is a clean path without extraneous spaces.
file_path = "/var/log/app/server.log"
3
Step 3 — Measure Entry Lengths with ncharCompute the character count of each log entry: entry_lengths <- nchar(logs) This returns an integer vector of the same length as logs. You can verify that all entries are at least 20 characters long.
entry_lengths = c(33, 30, 31, 28)
4
Step 4 — Extract the Date Prefix with substrEach log starts with a date in YYYY-MM-DD format occupying positions 1 through 10: dates <- substr(logs, 1, 10) Because substr is vectorized, it extracts positions 1–10 from every element simultaneously.
dates = c("2025-01-10", "2025-01-10", "2025-01-11", "2025-01-11")
5
Step 5 — Filter for ERROR Entries with grep and greplFind which entries contain "ERROR" using both approaches: error_idx <- grep("ERROR", logs) # integer indices error_mask <- grepl("ERROR", logs) # logical vector error_entries <- logs[error_mask] # subsetting We used fixed = FALSE (the default), which means "ERROR" is treated as a regex. Since the pattern contains no metacharacters, literal and regex matching give identical results here.
error_idx = c(2, 4); error_mask = c(FALSE, TRUE, FALSE, TRUE); error_entries = c("2025-01-10 ERROR disk full", "2025-01-11 ERROR timeout")
6
Step 6 — Combine Results with paste (collapse)Create a comma-separated summary of the unique dates that had errors: error_dates <- unique(substr(logs[error_mask], 1, 10)) summary_msg <- paste("Errors on:", paste(error_dates, collapse = ", ")) The inner paste with collapse reduces the date vector to a single string; the outer paste prepends the label.
summary_msg = "Errors on: 2025-01-10, 2025-01-11"

Strengths, Limitations & Common Pitfalls

Base-R string functions are powerful and ubiquitous, but they are not without sharp edges. The table below catalogs the most important strengths alongside limitations that routinely trip up both beginners and intermediate R programmers. Understanding these trade-offs will help you decide when base R suffices and when reaching for a package like stringr is justified.

Strengths and limitations of base-R string functions
StrengthsLimitations / Pitfalls
Zero dependencies — available in every R installation out of the boxInconsistent naming: nchar vs substr vs grep — no unified prefix
Fully vectorized, enabling concise, loop-free code over large vectorsSilent vector recycling can produce incorrect results without warning when vector lengths don't align
paste automatically coerces non-character arguments to characternchar(NA) returns NA, not 0 — this propagates missingness unexpectedly
grep/grepl support full POSIX regular expressions nativelyRegex is the default; forgetting fixed = TRUE when matching literal special characters (e.g., ".") causes subtle bugs
Efficient C-level implementation for most operationsNo built-in support for Unicode normalization, locale-aware collation, or boundary detection — stringi is needed for those
WATCH OUT
The most common mistake is confusing sep and collapse in paste(). Think of sep as the glue between arguments within each element, and collapse as the glue between elements of the resulting vector. If you want c("a", "b", "c") to become "a, b, c", you need paste(x, collapse = ", "), not paste(x, sep = ", "). The latter does nothing visible because sep operates between positional arguments, and there is only one.

Connection to Advanced String Processing

The base-R functions covered here form the lowest layer of R's text-processing stack. As your needs grow more complex—Unicode-aware tokenization, look-ahead/look-behind assertions, or consistent API design—you will encounter packages that build on or replace these primitives. The table below maps each base function to its counterpart in the stringr and stringi ecosystems, along with related base functions you may encounter in the wild.

Base R string functions and their ecosystem counterparts
Base Rstringr Equivalentstringi EquivalentRelated Base Functions
paste / paste0str_cstri_c / stri_joinsprintf, format
ncharstr_lengthstri_lengthnzchar (tests for empty strings)
substrstr_substri_subsubstring (allows vector of start/stop)
grep / greplstr_which / str_detectstri_detectregexpr, gregexpr, regmatches

Beyond these one-to-one mappings, more advanced text processing in R involves regular expression capture groups (via regmatches and regexec), string interpolation (via the glue package), and text mining frameworks like tidytext and quanteda. All of these, however, assume fluency with the base primitives. Mastering paste, nchar, substr, grep, and grepl is therefore not merely an introductory exercise but a prerequisite for everything that follows in R's text-processing landscape.

🔭 LOOKING AHEAD
In subsequent lessons you will learn gsub and sub for pattern-based replacement, strsplit for tokenization, and sprintf for C-style formatted output. Each builds naturally on the concepts introduced here.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain the difference between the sep and collapse arguments in paste(). Under what circumstances would using only sep fail to produce a single scalar string from a vector input?
PROBLEM 2BASIC CALCULATION
Given x <- c("apple", "banana", "cherry"), write a single R expression that returns the number of characters in each element. Then write another expression that extracts the first three characters from each element.
PROBLEM 3INTERMEDIATE
You have a vector of email addresses: emails <- c("alice@uni.edu", "bob@company.com", "carol@uni.edu", "dave@lab.org"). Using grepl, create a logical mask that identifies which emails belong to the domain uni.edu. Why should you consider using fixed = TRUE in this case?
PROBLEM 4APPLIED
You are processing a data frame of sensor readings where column names follow the pattern "sensor_01_temp", "sensor_02_humidity", "sensor_01_humidity", "sensor_03_temp". Write R code using grep (or grepl) to select only the column names that contain "temp", and then use substr to extract the sensor ID number (positions 8–9) from those names. Finally, use paste0 to construct labels of the form "T_01", "T_03".
PROBLEM 5CRITICAL THINKING
Consider the expression paste(c("x", "y"), 1:4, sep = "_"). Predict the output, explain the vector recycling that occurs, and discuss the potential risks of relying on silent recycling in production code. Then propose a defensive coding pattern that would raise an error if the vector lengths are incompatible.

Summary

R provides five essential base functions for string manipulation. paste() concatenates strings with a configurable sep delimiter and an optional collapse argument that reduces a vector to a scalar. paste0() is the zero-separator variant, ideal for building file paths and identifiers. nchar() counts the characters in each element (not to be confused with length(), which counts vector elements). substr() extracts substrings by 1-based inclusive position and can also perform in-place replacement.

For pattern matching, grep() returns integer indices of matching elements, while grepl() returns a logical vector—use the former for positional subsetting and the latter for boolean filtering. Both default to regex interpretation; set fixed = TRUE for safe literal matching. All five functions are fully vectorized and obey R's recycling rules, which makes them powerful but also demands care with mismatched vector lengths. These base primitives form the foundation for every advanced text-processing workflow in R.

Varsity Tutors • R Programming • Basic String Functions — Use basic string functions (paste/paste0, substr, nchar, grep/grepl) (intro)