Type a handful of messages by hand, then build a pattern you can test
source data loading
regular expressions
testing
Type small examples by hand and use them to find dates, email addresses, and order numbers in R.
Near the end of a support shift, a worker needs to connect incoming messages with the right orders. One message may contain two order numbers, another may contain an email address, and most contain neither. Reading a small inbox is easy. Repeating the job across hundreds of messages makes missed and mistaken matches more likely.
The worker starts by typing five messages by hand rather than exporting the full inbox. Typing them is the point: every expected answer fits on the screen, including the examples that should fail, and nothing arrives from a system that might have reformatted it. This small rehearsal set will help us build a text pattern, test its limits, and hide an address before the messages are shared.
TipWhat you will learn
By the end of this lesson, you will be able to:
search for an exact word or phrase;
describe a family of values such as order numbers or dates;
extract and replace matching text; and
turn hand-written examples into checks that catch mistakes.
Make a small rehearsal set
The five messages below are varied enough to pose real questions and short enough to check by eye. In R, c() combines values into a collection called a vector. The arrow <- saves that vector under the name messages.
library(stringr)library(tibble)library(dplyr)library(purrr)messages <-c("Alex likes to play guitar.","Email support@example.com before 17:30.","Order IDs: R-1042 and R-1043.","The café opens on 2026-08-28.","No identifiers here.")messages
[1] "Alex likes to play guitar."
[2] "Email support@example.com before 17:30."
[3] "Order IDs: R-1042 and R-1043."
[4] "The café opens on 2026-08-28."
[5] "No identifiers here."
R prints all five messages. The number in square brackets at the left marks the position where that line of output begins.
The messages are English, tagged en in the BCP 47 language tags used by web and publishing tools. That matters for two rules written later. The order ID rule assumes ASCII digits, and the ^ERROR: rule assumes a Latin-script label in capital letters. Neither assumption survives a move to Arabic-Indic digits or a script without case. The email and date rules are about shape and travel further, though the date rule still assumes a Gregorian calendar written largest unit first.
Search for exact text
The simplest search is the one that solves the problem with the fewest moving parts. Here we ask for the exact text "café" before introducing any flexible pattern.
str_subset() from stringr keeps the messages that match a pattern. messages says where to look, and fixed("café") says to treat "café" as ordinary text rather than as a regular expression.
fixed() is a good choice when the exact word, phrase, label, or punctuation is already known. It prevents punctuation from taking on a special pattern-matching meaning.
Ask a yes-or-no question
The support worker will eventually need one answer for every message. str_detect() asks whether each message contains a match, returning TRUE for yes and FALSE for no. We will display the answer as a tibble, a table that prints its size and each column’s type.
Only the third message contains "R-". We have found the likely message, but the rule is still too loose. It would also accept "R-" with no digits after it.
Describe a family of matches
The two visible order numbers share a shape that we can state one piece at a time:
the letter R;
a hyphen; and
exactly four digits.
A regular expression, often shortened to regex, is a compact description of a text pattern. The next expression asks for the whole order ID rather than its first two characters.
The output stays grouped by message. An empty entry means that no order number was found there. The third entry contains exactly the two IDs the worker could see in the rehearsal set.
This pattern can be read from left to right:
Parts of the order ID pattern
Part
Meaning
\b
Start or end at a word boundary.
R-
Match these two characters exactly.
[0-9]
Match one ASCII digit from 0 through 9.
{4}
Require the previous item four times.
\b
Stop at another word boundary.
R’s raw string notation, written here as r"(...)", keeps the backslashes readable. Without it, the same pattern would be written as "\\bR-[0-9]{4}\\b".
The order system defines its IDs with ASCII digits. In the ICU engine used by stringr, \d also matches decimal digits from other writing systems. Using [0-9] makes the rule explicit.
Find several kinds of information
Order IDs are only one kind of information mixed into the messages. The same rehearsal set also contains a date and an email address, so each gets a separate pattern.
The table keeps every match beside the message that supplied it. The underlying order_ids column is a list-column because each message can produce zero, one, or several IDs. The display copy turns each small character vector into readable text and prints No match where the extractor found nothing.
str_extract() returns only the first match, which is why the other two columns are named first_date and first_email.
message_number is a display position, not a durable identifier. Reordering the vector would change it. Records from a real system should keep the source’s message ID from the moment they are read.
The email pattern is intentionally modest. It recognizes the address in this example, but it does not validate every email address allowed by internet standards. Finding text that resembles an email and proving that an address is valid are different tasks.
Use the start and end of a string
A loose match can occur in the wrong place. Some labels matter only at the start of a text value, and some values must occupy the whole string. In these single-line examples, ^ means “the start of the string” and $ means “the end of the string.”
log_lines <-c("ERROR: file not found","INFO: job started","ERRORISH: not an error record")error_lines <-str_subset(log_lines, "^ERROR:")error_lines
[1] "ERROR: file not found"
The pattern requires the complete label ERROR: at the start. It leaves out ERRORISH, whose opening text and punctuation do not meet that rule.
Using both anchors checks a complete value from beginning to end. The next pattern also uses parentheses to capture the year, month, and day separately.
NA means “missing” in R. The last two inputs do not have the required four-digit year, two-digit month, and two-digit day as a complete value.
This pattern checks the arrangement of digits. It does not check the calendar. For example, 2026-99-99 has the expected shape but is not a real date.
Replace matching text
The worker may need to share an example message while keeping the sender’s address private. A pattern can replace matching text before the example is shared. The following code substitutes [email] for the address.
One message changed and one address was removed. Both numbers are printed, and so is the removed text itself. A redaction step that reports only its output cannot be audited: a pattern that matched half a message without warning would look exactly as successful. Read the removed material, count it, and compare that count with the number of matches the pattern found.
The address is gone, while the rest of each sentence stays unchanged. One successful example is not enough to establish that a redaction method protects private information. Test varied cases carefully, inspect the result, and follow the privacy rules that apply to the real records.
Turn examples into safety checks
Before searching the full inbox, the worker adds close failures beside the clear matches. The order ID must contain one R, one hyphen, and exactly four digits. mutate() from dplyr adds the actual test result as a new column.
id_cases <-tibble(input =c("R-0001","prefix R-1234 suffix","R-12","RR-1234","R-12345" ),expected =c(TRUE, TRUE, FALSE, FALSE, FALSE))id_cases <- id_cases |>mutate(actual =str_detect(input, order_id_pattern))knitr::kable( id_cases,col.names =c("Input", "Expected", "Actual"),caption ="Expected and actual order ID results",row.names =FALSE)
Expected and actual order ID results
Input
Expected
Actual
R-0001
TRUE
TRUE
prefix R-1234 suffix
TRUE
TRUE
R-12
FALSE
FALSE
RR-1234
FALSE
FALSE
R-12345
FALSE
FALSE
The expected and actual columns agree. A near miss such as R-12345 remains visible during testing rather than becoming a mistaken match later.
Reuse a tested pattern
The checked extraction will be used more than once, so we place it in a function. A function is a named set of steps that accepts an input and returns a result.
The first function preserves one result per message. The second function deliberately combines those results into a corpus-wide list and removes duplicates. Its name makes the loss of message boundaries explicit.
The distinction matters whenever a later question asks where a match came from. Keep the row-level result, then derive a flattened inventory from it when the inventory is the actual goal.
State which text the pattern accepts
Support messages may use accented letters or writing systems beyond English. In ICU, \p{L} means a code point classified as a letter and \p{M} means a combining mark. The second category matters because a visible accented letter can be stored as a base letter followed by a separate mark.
words <-c("cafe","café","naïve","李雷","e\u0301","hello-world")letters_and_marks_pattern <- r"(^(?:\p{L}\p{M}*)+$)"letters_and_marks <-str_detect( words, letters_and_marks_pattern)knitr::kable(tibble(text = words,accepted_by_pattern = letters_and_marks ),col.names =c("Text", "Accepted by this pattern"),caption ="Checking the stated letter-and-mark rule",row.names =FALSE)
Checking the stated letter-and-mark rule
Text
Accepted by this pattern
cafe
TRUE
café
TRUE
naïve
TRUE
李雷
TRUE
é
TRUE
hello-world
FALSE
The first five examples follow the stated rule. The fifth displays as é but uses e plus a combining accent. hello-world does not match because a hyphen is punctuation. This check answers a specific text question; it is not a rule for deciding whether a person’s name or a word is valid.
NoteTechnical detail: R has more than one pattern engine
The software that interprets a regular expression is called a pattern engine. Different engines do not agree on every feature.
Base R uses POSIX extended regular expressions by default and PCRE2 when perl = TRUE. The stringr package uses the ICU engine through stringi. These engines share common syntax, but some Unicode rules and advanced features differ.
The examples in this lesson use stringr for regular expressions. Test a pattern again if you move it to a different engine. Avoid useBytes = TRUE for normal text because byte-level matching can split one UTF-8 character into several pieces.
Carry the tested pattern into real work
The support worker now has a repeatable process rather than a pattern copied straight into the full inbox:
Write a few short examples that should match.
Add similar examples that must not match.
Use fixed() when you only need exact text.
Build a regular expression one piece at a time.
Inspect the text you extract before replacing anything.
Record the expected answers in the lesson’s hidden checks.
Add a new example whenever the pattern surprises you.
What to remember
Exact searches are simpler than regular expressions and should be used when they solve the problem.
A regular expression describes a family of possible matches.
Small hand-written examples make a pattern easier to understand.
Expected failures are as useful as expected matches.
Name the language and script a pattern assumes; digits and letter case do not behave the same everywhere.
Count and read what a replacement removed, not only what it left behind.
A published example should stop the build when its result changes.
The five typed messages end with both order IDs found, one address removed, and that removal counted. Keep the same two small piles beside any new pattern: examples it must accept and examples it must refuse.