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]}")| Base | stringr |
|---|---|
grep | str_detect, str_subset, str_which |
sub / gsub | str_replace / str_replace_all |
regexpr plus regmatches | str_extract, str_match |
strsplit | str_split, str_split_fixed |
sprintf | str_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))ymdand friends fail with a warning and a missing value when the input does not match, rather than silently guessing.- A
Dateis a day count with no time zone. APOSIXctis 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.Related
Writing functions, control flow and the apply family Debugging R: warnings, factors, NA and performance traps
Last refreshed 2026-09-18.