Using lubridate to Work with Dates and Times in R
Using lubridate to handle dates and times in R replaces the awkward base R date functions with intuitive, verb-shaped alternatives. Functions like ymd(), dmy(), and mdy() parse dates by guessing the format from the order of components, while period(), duration(), and interval() model time intervals correctly across DST transitions and leap days. This tutorial covers parsing, arithmetic, and time zone handling with practical examples you can apply immediately.
What you’ll learn
This tutorial covers the key concepts and practical techniques for working with Working with dates in R using lubridate. By the end, you will know how to apply the core functions in real data analysis workflows.
Installing and loading lubridate
First, install and load lubridate from CRAN:
install.packages("lubridate")
library(lubridate)
lubridate is part of the tidyverse, so if you have tidyverse loaded, you already have lubridate:
library(tidyverse)
Parsing dates and times
The hardest part of working with dates is often just getting R to recognize them. lubridate makes this straightforward with functions that match common date formats.
Automatic parsing functions
lubridate provides functions named after the order of date components:
| Function | Format | Example |
|---|---|---|
ymd() | Year-Month-Day | 2024-03-15 |
mdy() | Month-Day-Year | March 15, 2024 |
dmy() | Day-Month-Year | 15 March 2024 |
ymd_hms() | With time | 2024-03-15 14:30:00 |
# Parse dates from different formats
ymd("2024-03-15")
# [1] "2024-03-15"
mdy("March 15, 2024")
# [1] "2024-03-15"
dmy("15-03-2024")
# [1] "2024-03-15"
# Parse datetime with time
ymd_hms("2024-03-15 14:30:45")
# [1] "2024-03-15 14:30:45 UTC"
# Shorter datetime formats
ymd_hm("2024-03-15 14:30")
ymd_h("2024-03-15 14")
Parsing from character vectors
All lubridate parsing functions are vectorized — pass a character vector and you get back a vector of Date or POSIXct values. This is the standard pattern for converting a date column in a data frame with mutate(). The function automatically detects separators (dashes, slashes, dots, or no separator at all) within each call. Non-parseable strings return NA with a warning.
dates <- c("2024-01-15", "2024-02-20", "2024-03-15")
ymd(dates)
#> [1] "2024-01-15" "2024-02-20" "2024-03-15"
datetimes <- c("2024-03-15 09:30:00", "2024-03-15 10:45:00")
ymd_hms(datetimes)
#> [1] "2024-03-15 09:30:00 UTC" "2024-03-15 10:45:00 UTC"
Handling mixed formats
When a single column contains dates in different formats — common in datasets assembled from multiple sources — parse_date_time() accepts a vector of format orders and tries each one until parsing succeeds. This is more flexible than the single-format ymd() family and handles the messy reality of real-world data. For custom formats, parse_date_time(x, orders = "%B %d, %Y") also accepts strptime-style format codes.
mixed <- c("2024-01-15", "15/03/2024", "2024.03.20")
parse_date_time(mixed, orders = c("ymd", "dmy", "ymd"))
#> [1] "2024-01-15 UTC" "2024-03-15 UTC" "2024-03-20 UTC"
Extracting date components
Accessor functions return individual components as numeric values. These are the building blocks for time-based analysis: filter by year, group by month, compute day-of-week patterns, or extract hour-of-day for traffic analysis. Adding label = TRUE returns ordered factors with human-readable abbreviations, and these accessors also work as assignment targets — you can modify a date component in place without reconstructing the entire date.
date <- ymd_hms("2024-03-15 14:30:45")
year(date) #> 2024
month(date) #> 3
month(date, label = TRUE) #> Mar (ordered factor)
day(date) #> 15
hour(date) #> 14
minute(date) #> 30
second(date) #> 45
wday(date) #> 6 (1=Sunday)
wday(date, label = TRUE) #> Fri
# Accessors also allow assignment to modify components
date <- ymd("2024-03-15")
month(date) <- 6
date
#> [1] "2024-06-15"
Date arithmetic
lubridate makes date arithmetic intuitive with functions like days(), weeks(), months(), and years():
Adding and subtracting time
Date arithmetic uses helper functions — days(), weeks(), months(), years() — that produce objects lubridate understands. Adding them to a date or datetime shifts the value forward by the specified amount, handling month boundaries and leap years correctly:
# Start date
start <- ymd("2024-03-15")
# Add time
start + days(10)
# [1] "2024-03-25"
start + weeks(2)
# [1] "2024-03-29"
start + months(1)
# [1] "2024-04-15"
start + years(1)
# [1] "2025-03-15"
# Subtract time
start - days(5)
# [1] "2024-03-10"
Difference between dates
Use %--% to create an interval between two dates, then convert it to a duration or period to express the span in the units you need. The interval stores both endpoints, so conversions to different units are always computed from the exact start and end dates:
start <- ymd("2024-01-01")
end <- ymd("2024-03-15")
# Calculate difference
interval <- start %--% end
interval
# [1] 2024-01-01 UTC--2024-03-15 UTC
# Get the span in different units
as.duration(interval)
# [1] "74d 0H 0M 0S"
as.period(interval)
# [1] "2 months and 14 days"
# Quick calculations
end - start
# Time difference of 74 days
Rounding dates
Round dates to nearby boundaries with floor_date(), ceiling_date(), and round_date(). These functions take a datetime and a unit (like “day”, “hour”, or “month”) and snap the value to the nearest boundary in that unit:
datetime <- ymd_hms("2024-03-15 14:30:45")
floor_date(datetime, "day") # Round down to day
# [1] "2024-03-15 UTC"
ceiling_date(datetime, "day") # Round up to day
# [1] "2024-03-16 UTC"
round_date(datetime, "hour") # Round to nearest hour
# [1] "2024-03-15 15:00:00 UTC"
Handling time zones
Time zones can be tricky. lubridate makes them manageable:
Setting time zones
Use tz in the parsing function to set the timezone at creation, then with_tz() to convert between timezones. The underlying instant (seconds since 1970-01-01) never changes during conversion — only the displayed clock time adjusts to the target timezone:
# Parse with specific timezone
datetime <- ymd_hms("2024-03-15 14:30:00", tz = "America/New_York")
datetime
# [1] "2024-03-15 14:30:00 EST"
# Convert to another timezone
with_tz(datetime, "UTC")
# [1] "2024-03-15 19:30:00 UTC"
with_tz(datetime, "Europe/London")
# [1] "2024-03-15 18:30:00 GMT"
Common time zone operations
Common time zone operations include checking the system timezone, listing available timezone names, and forcing a timezone onto a naive datetime. force_tz() is the dangerous version — it changes the clock’s interpretation without adjusting the instant:
# Get current timezone
Sys.timezone()
# [1] "UTC"
# See available timezones
length(OlsonNames())
# [1] 599
# Parse and force timezone (even if string has no tz)
force_tz(ymd_hms("2024-03-15 14:30:00"), tzone = "America/Los_Angeles")
# [1] "2024-03-15 14:30:00 PST"
Practical examples
Example 1: calculate age
calculate_age <- function(birth_date) {
birth <- ymd(birth_date)
today <- today()
age <- year(today) - year(birth)
# Subtract 1 if birthday hasn't occurred this year
month(today) < month(birth) |
(month(today) == month(birth) & day(today) < day(birth))
age - 1
}
calculate_age("1990-05-15")
# [1] 35
Example 2: business days between dates
library(lubridate)
# Create date range
start <- ymd("2024-03-01")
end <- ymd("2024-03-31")
# Get all days in March
all_days <- seq(start, end, by = "day")
# Remove weekends
business_days <- all_days[!wday(all_days) %in% c(1, 7)]
length(business_days)
# [1] 21
Example 3: find last day of month
# Last day of March 2024
ceiling_date(ymd("2024-03-15"), "month") - days(1)
# [1] "2024-03-31"
# Or use the dedicated function
ceiling_date(ymd("2024-03-15"), "month") - ddays(1)
# [1] "2024-03-31 23:59:59 UTC"
Example 4: parse dates from real data
# Simulating reading from a CSV
library(dplyr)
sample_data <- tibble(
date_str = c("2024-01-15", "2024-02-20", "2024-03-10", "2024-04-05")
)
# Parse and extract components
sample_data %>%
mutate(
date = ymd(date_str),
year = year(date),
month = month(date, label = TRUE),
day = day(date),
quarter = quarter(date),
week = week(date)
)
# # A tibble: 4 × 7
# date_str date year month day quarter week
# <chr> <date> <dbl> <ord> <int> <int> <int>
# 1 2024-01-15 2024-01-15 2024 Jan 15 1 3
# 2 2024-02-20 2024-02-20 2024 Feb 20 1 8
# 3 2024-03-10 2024-03-10 2024 Mar 10 1 11
# 4 2024-04-05 2024-04-05 2024 Apr 5 2 14
Extracting components
year(dt), month(dt), day(dt), hour(dt), minute(dt), second(dt) extract components. These also work as assignment: month(dt) <- 6 changes the month to June. All are vectorized.
wday(dt, label = TRUE) returns the day of week as an ordered factor (“Mon”, “Tue”, …). wday(dt, label = FALSE) returns an integer (1 = Sunday by default; set week_start = 1 for Monday). yday(dt) returns the day of year (1-366). week(dt) returns the week number; isoweek(dt) uses ISO 8601 week numbering.
quarter(dt) returns the quarter (1-4). semester(dt) returns 1 or 2. floor_date(dt, unit = "month") rounds down to the start of the month; ceiling_date(dt, unit = "week") rounds up. round_date(dt, unit = "hour") rounds to the nearest hour.
Arithmetic with periods and durations
Lubridate distinguishes periods (human-time units) from durations (physical time). months(3) is a period (the next calendar quarter, variable in seconds). dmonths(3) is a duration (exactly 3 * 30.4375 * 86400 seconds). The distinction matters around DST transitions and month boundaries.
dt + years(1) adds one calendar year, preserving the day of month. dt + dyears(1) adds exactly 365.25 * 86400 seconds. For business applications (billing dates, contract renewals), use periods. For physics and data science (elapsed time, timestamps), use durations.
difftime(end, start, units = "days") computes differences, returning a difftime object. as.numeric(difftime(...)) extracts the numeric value. interval(start, end) creates an interval object; as.duration(interval(start, end)) converts to seconds; as.period(interval(start, end), unit = "month") converts to a period.
Time zones
with_tz(dt, tzone = "America/New_York") changes the display time zone without changing the underlying instant. force_tz(dt, tzone = "America/Chicago") changes the interpretation: it reinterprets the displayed time as being in Chicago, changing the underlying instant.
Sys.timezone() returns the system time zone. OlsonNames() lists all valid time zone names. Always store timestamps in UTC internally and convert to local time for display: with_tz(utc_dt, tzone = user_timezone).
lubridate::now(tzone = "UTC") returns the current time in UTC. lubridate::today() returns today’s date with no time component.
Working with date sequences
seq(from = ymd("2024-01-01"), to = ymd("2024-12-31"), by = "month") generates a monthly sequence. by = "week", by = "quarter", by = "year" work similarly. by = 7 (integer) generates a sequence with 7-day steps.
%within% tests whether a date falls within an interval: ymd("2024-06-15") %within% interval(ymd("2024-01-01"), ymd("2024-12-31")) returns TRUE.
For business days, bizdays::create.calendar() with a holiday list enables bizdays::add.bizdays(date, n) to add n business days. The timeDate package also provides holiday calendars for many countries.
Why date handling is hard
Dates and times are more complex than they appear. A date string like “01/02/03” is ambiguous, is it January 2, 2003, or February 1, 2003, or February 3, 2001? Different regions use different date formats. Times complicate matters further: UTC offset rules change, daylight saving time creates discontinuities, and some timezone conversions involve non-integer hour offsets. Doing date arithmetic correctly requires awareness of all these complications.
lubridate provides a consistent interface that isolates you from most of this complexity. Its parsing functions accept format strings that match common date representations. Its arithmetic functions handle the edge cases of month and year arithmetic. Its timezone functions use the IANA timezone database to apply the correct DST rules for each location. You still need to know the right function to call, but the functions behave correctly once you do.
Parsing ambiguous dates
The order of year, month, and day in a date string determines which lubridate parsing function to use. The function names encode the parsing order: ymd parses year-month-day, mdy parses month-day-year, dmy parses day-month-year. These functions are forgiving about separators — slashes, dashes, spaces, and periods all work. The parser infers the separator from the string.
When a dataset contains date strings in multiple formats, or the format is unknown, use parse_date_time with a list of possible format strings. It tries each format in order and returns the first successful parse. For large datasets with inconsistent dates, this is slower than a single-format parser but catches formats that the default parsers would miss. Always check parsed dates by plotting them on a timeline or looking at min/max — parsing errors often produce implausible values like dates far in the future or past.
Month and year arithmetic
Adding months or years to a date requires care because calendar months have different lengths. Adding one month to January 31 cannot return February 31 — that date does not exist. lubridate’s %m+% operator handles this by rolling back to the last day of the month: January 31 %m+% months(1) returns February 28 (or 29 in a leap year). Using the plus operator directly does not handle this edge case and returns NA.
Year arithmetic has the same edge case for February 29 in leap years. The rollback behavior of %m+% applies consistently. For financial and business date calculations where end-of-month dates are common, using %m+% instead of + months() prevents hard-to-reproduce bugs that only appear at the end of short months.
Summary
lubridate transforms date handling from a headache into a pleasure:
- Parsing: Use
ymd(),mdy(),dmy()and their datetime variants - Extraction:
year(),month(),day(),hour(),wday() - Arithmetic: Add/subtract with
days(),months(),years() - Intervals: Use
%--%to calculate spans between dates - Time zones:
with_tz()andforce_tz()for timezone conversion - Rounding:
floor_date(),ceiling_date(),round_date()
With these tools, you’ll handle any date-related task with confidence. In the next tutorial, we’ll cover forcats for handling categorical variables—another common data wrangling challenge.
Next steps
Now that you understand working with dates in r using lubridate, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.
See also
- String Manipulation with stringr — another tidyverse package for data cleaning
- Importing Data with readr — parse dates during file import with col_types
- dplyr Basics — filter and group_by by date columns