Quick summary
Summarize this blog with AI
Introduction
substring() selects characters by one-based, inclusive positions. It is appropriate for a known fixed layout. Assigning through substring replaces existing character positions; it does not expand a five-character field into a longer word. Use pattern replacement or reconstruction when the string length must change.
Tested with R 4.5.1. Run the following code blocks in order. All inputs are included.
Extract a fixed-width field
The start and end positions both belong to the result. Omitting the end takes the rest of the string.
text <- "Hello, World!"
print(substring(text, 8, 12)) # "World"
print(substring(text, 8)) # "World!"
stopifnot(identical(substring(text, 8, 12), "World"))
fields <- c("2026-01-15", "2026-12-03", NA_character_)
months <- substring(fields, 6, 7)
stopifnot(identical(months, c("01", "12", NA_character_)))
Extracting positions 6–7 does not validate a date. Parse and validate dates with an explicit date format when calendar validity matters; fixed slices alone accept impossible months.
Understand replacement width
The replacement is limited by the selected region and existing string. Compare a fixed-width replacement with a length-changing operation.
fixed_width <- text
substring(fixed_width, 8, 12) <- "Universe"
print(fixed_width) # "Hello, Unive!"
stopifnot(identical(fixed_width, "Hello, Unive!"))
expanded <- sub("World", "Universe", text, fixed = TRUE)
print(expanded) # "Hello, Universe!"
stopifnot(identical(expanded, "Hello, Universe!"))
The truncation is normal substring replacement behavior, not a successful insertion of the whole replacement word. For repeated replacement throughout text, see gsub examples.
Find variable-width fields by their pattern
Hard-coded log positions break when prefixes change. Extract the matched error-code field first, then remove its fixed label.
logs <- c("[ERROR] Error code 500", "Error code 404", "Task complete", NA_character_)
pattern <- "Error code [0-9]{3}"
found <- regexpr(pattern, logs)
code <- rep(NA_character_, length(logs))
has_match <- !is.na(logs) & found > 0
field <- regmatches(logs[has_match], regexpr(pattern, logs[has_match]))
code[has_match] <- substring(field, nchar("Error code ") + 1L)
print(code) # "500" "404" NA NA
stopifnot(identical(code, c("500", "404", NA_character_, NA_character_)))
This pattern is for the demonstrated log contract, not every possible log format. Missing and unmatched records keep their positions. For a reliable structured logging pipeline, prefer explicit fields to parsing human-readable messages.
Check vector and boundary behavior
substring can recycle argument vectors; choose lengths deliberately. On ordinary UTF-8 text it counts characters, not user-perceived grapheme clusters.
texts <- c("alpha", "bravo")
print(substring(texts, c(1, 2), c(2, 4))) # "al" "rav"
stopifnot(identical(substring(texts, c(1, 2), c(2, 4)), c("al", "rav")))
stopifnot(identical(substring("abc", 5, 7), ""))
stopifnot(identical(substring("abc", 3, 2), ""))
stopifnot(identical(substring("", 1, 3), ""))
stopifnot(is.na(substring(NA_character_, 1, 3)))
stopifnot(length(substring(character(0), 1, 3)) == 0)
stopifnot(identical(substring(enc2utf8("café"), 4, 4), "é"))
A visually single symbol may contain multiple Unicode code points. Do not use byte positions from a different encoding as character positions. Validate format and lengths before applying a fixed-width rule to a new data source.