Strings and dates with stringr and lubridate

Match, extract and replace text with a consistent API, then parse, shift and compare dates without losing track of time zones.

String manipulation

library(stringr)

x <- c("Order A-1042 shipped", "Order B-88 pending", NA)

str_detect(x, "A-[0-9]+")                 # logical, NA stays NA
str_subset(x, "shipped")                  # the matching elements
str_extract(x, "[A-Z]-[0-9]+")            # first match per element
str_match(x, "([A-Z])-([0-9]+)")          # capture groups in a matrix

str_replace(x, "Order ", "ORD-")
str_replace_all(x, "[aeiou]", "*")
str_split_fixed(x, " ", n = 3)

str_trim("  padded  ")
str_pad("7", width = 3, side = "left", pad = "0")
str_glue("value: {x[2]}")
Basestringr
grepstr_detect, str_subset, str_which
sub / gsubstr_replace / str_replace_all
regexpr plus regmatchesstr_extract, str_match
strsplitstr_split, str_split_fixed
sprintfstr_glue, str_pad

stringr functions take the string first and the pattern second, keep NA as NA rather than dropping it, and are vectorised over the input. That consistency removes most of the argument-order mistakes base R invites.

Parsing dates

library(lubridate)

d1 <- ymd("2026-09-18")
d2 <- dmy("18/09/2026")
d3 <- mdy_hms("09-18-2026 14:30:00", tz = "Europe/London")

# ambiguous input can be parsed in one step when the format varies
d4 <- parse_date_time(c("20260918", "18-09-2026"), orders = c("ymd", "dmy"))

year(d1); month(d1); wday(d1, label = TRUE, week_start = 1)
floor_date(d1, "month")
ceiling_date(d1, "month") - days(1)

# days, weeks and months are periods; seconds and hours are durations
d1 + months(1)
d2 + dweeks(2)
as.duration(interval(d1, d2))
  • ymd and friends fail with a warning and a missing value when the input does not match, rather than silently guessing.
  • A Date is a day count with no time zone. A POSIXct is a number of seconds with a zone attached, and printing converts it to local time.
  • interval() gives a start and end; dividing two intervals gives a ratio, and %within% tests membership.

Time zones and arithmetic

x <- ymd_hms("2026-03-29 00:30:00", tz = "Europe/London")
x + hours(2)                     # crosses the DST jump: the wall clock skips an hour
x + dhours(2)                    # exactly 7200 seconds later
force_tz(x, "UTC")               # reinterpret in another zone
with_tz(x, "America/New_York")   # convert to another zone
⚠️
Adding days(1) is not always adding 86400 seconds: on a daylight-saving boundary the wall clock shifts. Use periods (days, months) when you mean calendar time and durations (ddays, dhours) when you mean elapsed time, and never store a local time without its zone.

FAQ

Why does as.Date on a factor give wrong results?
A factor is converted through its integer codes, not its labels. Convert with as.character() first, or pass format= explicitly: as.Date(as.character(f), format = "%d/%m/%Y").
How do I keep the original time zone when reading a CSV?
The column arrives as text. Parse it with ymd_hms(tz = "...") and set the zone explicitly. If you omit the zone, lubridate uses UTC, and every subsequent display is offset from what the user saw.

Writing functions, control flow and the apply family Debugging R: warnings, factors, NA and performance traps

Last refreshed 2026-09-18.