Manual examples and pattern matching

Type a small set of messages by hand. Examine a text pattern.

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. Processing hundreds of messages makes missed and mistaken matches more likely.

The worker types five messages by hand rather than exporting the full inbox. Every expected answer fits on the screen. Some examples should fail. Manual entry also avoids changes that another system might make to the text. We will use these examples to create a pattern and examine its limits. Then we will hide an address before sharing the messages.

TipWhat you will learn

By the end of this lesson, you will be able to:

  • search for an exact word or phrase.
  • describe a set of values such as order numbers or dates.
  • extract and replace matching text.
  • create checks for mistakes from hand-written examples.

Make a small example set

The five messages contain different cases and are short enough for visual inspection. 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. Web and publishing tools identify English with the BCP 47 language tag en. The language matters for two later rules. The order ID rule assumes ASCII digits, and the ^ERROR: rule assumes a Latin-script label in capital letters. These assumptions do not apply to Arabic-Indic digits or a script without case.

The email and date patterns can apply more broadly because they describe text structure. The date rule still assumes a Gregorian calendar with the largest unit first.

Search for exact text

Use an exact search when the required text is known. Here we search for "cafΓ©" before introducing a flexible pattern.

cafe_message <- str_subset(messages, fixed("cafΓ©"))

cafe_message
[1] "The cafΓ© opens on 2026-08-28."

str_subset() from stringr keeps messages that match a pattern. The messages argument supplies the text. The fixed("cafΓ©") argument treats "cafΓ©" as ordinary text rather than a regular expression.

fixed() is a good choice when the exact word, phrase, label, or punctuation is already known. It prevents punctuation from acquiring 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 in a table.

has_order_prefix <- str_detect(messages, fixed("R-"))
Which messages contain R-?
Message Contains R-
Alex likes to play guitar. FALSE
Email support@example.com before 17:30. FALSE
Order IDs: R-1042 and R-1043. TRUE
The cafΓ© opens on 2026-08-28. FALSE
No identifiers here. FALSE

Only the third message contains "R-". The search found the likely message, but the rule does not require a complete order ID. It also accepts "R-" without subsequent digits.

Describe a set of matches

The two visible order numbers have the same structure:

  • the letter R.
  • a hyphen.
  • 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.

order_id_pattern <- r"(\bR-[0-9]{4}\b)"

order_ids_by_message <- str_extract_all(
  messages,
  order_id_pattern
)

order_ids_by_message
[[1]]
character(0)

[[2]]
character(0)

[[3]]
[1] "R-1042" "R-1043"

[[4]]
character(0)

[[5]]
character(0)

The output groups results by message. An empty entry means that the pattern found no order number in that message. The third entry contains the two IDs visible in the example set.

Read this pattern 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 raw notation, R requires "\\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

The messages also contain a date and an email address. Each information type uses a separate pattern.

date_pattern <- r"(\b[0-9]{4}-[0-9]{2}-[0-9]{2}\b)"
email_pattern <- r"([[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,})"

message_table <- tibble(
  message_number = seq_along(messages),
  message_text = messages
)

extracted_summary <- message_table |>
  mutate(
    order_ids = str_extract_all(
      message_text,
      order_id_pattern
    ),
    first_date = str_extract(
      message_text,
      date_pattern
    ),
    first_email = str_extract(
      message_text,
      email_pattern
    )
  )

extracted_for_display <- extracted_summary |>
  mutate(
    order_ids = map_chr(
      order_ids,
      \(ids) str_c(ids, collapse = ", ")
    ),
    order_ids = if_else(order_ids == "", "No match", order_ids),
    first_date = coalesce(first_date, "No match"),
    first_email = coalesce(first_email, "No match")
  )
Matches kept with the message that supplied them
Message Text Order IDs First date First email
1 Alex likes to play guitar. No match No match No match
2 Email support@example.com before 17:30. No match No match support@example.com
3 Order IDs: R-1042 and R-1043. R-1042, R-1043 No match No match
4 The cafΓ© opens on 2026-08-28. No match 2026-08-28 No match
5 No identifiers here. No match No match No match

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 converts each small character vector to 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 has limited coverage. It recognizes the address in this example, but it does not validate every email address that is valid under 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 excludes ERRORISH, whose opening text and punctuation do not meet that rule.

Using both anchors requires a complete match from beginning to end. The next pattern also uses parentheses to capture the year, month, and day separately.

complete_date_pattern <- r"(^([0-9]{4})-([0-9]{2})-([0-9]{2})$)"
date_inputs <- c(
  "2026-08-28",
  "2026-8-28",
  "due 2026-08-28"
)

date_parts <- str_match(
  date_inputs,
  complete_date_pattern
)
Complete-date checks
Input Complete match Year Month Day
2026-08-28 2026-08-28 2026 08 28
2026-8-28 NA NA NA NA
due 2026-08-28 NA NA NA NA

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 requires the stated arrangement of digits. It applies no calendar check. 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 without the sender’s address. The following code substitutes [email] for the matching address before sharing.

redaction_report <- message_table |>
  mutate(
    removed_addresses = str_extract_all(
      message_text,
      email_pattern
    ),
    shared_version = str_replace_all(
      message_text,
      email_pattern,
      "[email]"
    ),
    changed = shared_version != message_text
  )

changed_messages <- sum(redaction_report$changed)
Which messages the redaction step altered
Message Changed Version to share
1 FALSE Alex likes to play guitar.
2 TRUE Email [email] before 17:30.
3 FALSE Order IDs: R-1042 and R-1043.
4 FALSE The cafΓ© opens on 2026-08-28.
5 FALSE No identifiers here.

The report also keeps the removed text for inspection:

removed_addresses <- redaction_report$removed_addresses |>
  list_c()

removed_addresses
[1] "support@example.com"

The code changed one message and removed one address. The output shows both counts and the removed text. An output-only report cannot show whether a redaction pattern removed too much text.

Read the removed material. Count the removed matches. 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. Run checks on varied cases. Examine the result. Follow the privacy rules that apply to the real records.

Create safety checks from examples

Before searching the full inbox, the worker adds similar invalid inputs beside the valid 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))
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. Invalid inputs such as R-12345 remain visible during checks rather than becoming mistaken matches later.

Reuse a pattern after these checks

After these checks, we place the extraction in a function that we can call again. A function is a named set of steps that accepts an input and returns a result.

extract_order_ids_by_message <- function(text, pattern) {
  str_extract_all(text, pattern)
}

collect_unique_order_ids <- function(text, pattern) {
  extract_order_ids_by_message(text, pattern) |>
    list_c() |>
    unique()
}

ids_by_message <- extract_order_ids_by_message(
  messages,
  order_id_pattern
)
unique_order_ids <- collect_unique_order_ids(
  messages,
  order_id_pattern
)

unique_order_ids
[1] "R-1042" "R-1043"

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 when a later question concerns a match’s source. Keep the row-level result. Create a combined inventory only when the task requires one.

State which text the pattern accepts

Support messages may use accented letters or writing systems beyond English. In ICU, \p{L} identifies code points classified as letters. The \p{M} pattern identifies combining marks. A visible accented letter can consist of 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
)
Checks of 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

A pattern engine is software that interprets a regular expression. Different engines do not support identical features.

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. Run checks on 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.

Apply the pattern after these checks

The support worker can use this process before searching the full inbox:

  1. Write a few short examples that should match.
  2. Add similar examples that must not match.
  3. Use fixed() when you only need exact text.
  4. Build a regular expression one piece at a time.
  5. Examine the text you extract before replacing anything.
  6. Record the expected answers in the lesson’s hidden checks.
  7. Add an example whenever the pattern produces an unexpected result.

What to remember

  • Use exact searches instead of regular expressions when they solve the problem.
  • A regular expression describes a set 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 that a pattern assumes. Digit and case rules differ between writing systems.
  • Count the removed matches. Read the removed text as well as the retained text.
  • A published example should stop the build when its result changes.

The pattern found both order IDs and removed one address. The report records that removal. For each new pattern, keep examples that must match and examples that must not match.

Sources