๐ R Dates & Times
๐ง What this note covers
Dates and times feel simple on the surface but tend to hide a surprising amount of complexity, from time zones to leap years to varying month lengths. This note covers Rโs base Date and POSIXct classes for handling calendar dates and precise timestamps, and then introduces the lubridate package, which makes almost all of this dramatically easier and less error prone.
๐ The Date class
Rโs built in Date class represents a calendar date, with no time of day attached at all, internally stored as the number of days since January 1st, 1970.
today <- Sys.Date()
print(today)
class(today) # "Date"
my_date <- as.Date("2026-08-10") # converts text into a real Date objectDate parsing depends on the format matching
as.Date()expects the text to be in โYYYY-MM-DDโ format by default. If your date is formatted differently, such as โ10/08/2026,โ you must explicitly tell R the format it should expect, otherwise the conversion will either fail outright or, worse, silently produce a wrong date.
as.Date("10/08/2026", format = "%d/%m/%Y")
# correctly interpreted as the 10th of August, 2026Common format codes
| Code | Meaning | Example |
|---|---|---|
%Y | 4 digit year | 2026 |
%y | 2 digit year | 26 |
%m | 2 digit month | 08 |
%d | 2 digit day | 10 |
%B | Full month name | August |
%b | Abbreviated month name | Aug |
%A | Full weekday name | Monday |
%H | Hour, 24 hour format | 14 |
%M | Minute | 30 |
%S | Second | 00 |
Reading format codes is like matching a template to the text
A helpful way to think about the format string is that you are drawing a template over your actual date text, marking which part is which. If your text is โ10-Aug-2026,โ your format string needs to be
"%d-%b-%Y", matching piece for piece in the exact same order and using the exact same separator characters.
โฑ๏ธ The POSIXct class for date and time together
When you need both a date and a specific time of day, R uses the POSIXct class instead, which also carries time zone information.
now <- Sys.time()
print(now)
class(now) # "POSIXct" "POSIXt"
my_datetime <- as.POSIXct("2026-08-10 14:30:00", tz = "Asia/Kolkata")Why time zones matter even if you never leave home
Even if you always work with data from a single location, servers, cloud storage, and many APIs frequently store or return timestamps in UTC (Coordinated Universal Time) by default. Explicitly setting the
tzargument whenever you parse a timestamp is a good habit that avoids subtle, hours-long offset bugs that are notoriously hard to trace later.
๐งฎ Date arithmetic
One of the genuinely convenient features of Rโs date classes is that basic arithmetic just works correctly, automatically accounting for things like different month lengths and leap years.
as.Date("2026-08-10") + 30 # correctly adds 30 days, rolling over into September
as.Date("2026-08-10") - as.Date("2026-01-01") # returns a "difftime" object, "221 days"
as.numeric(as.Date("2026-08-10") - as.Date("2026-01-01")) # converts that difference into a plain number๐งฉ Extracting parts of a date
d <- as.Date("2026-08-10")
format(d, "%Y") # "2026", extracts just the year, as text
format(d, "%B") # "August", the full month name
weekdays(d) # "Monday", the day of the week๐ฆ lubridate: making date handling far easier
The lubridate package, another core tidyverse tool, offers much friendlier functions for the exact same tasks, and is what most modern R code actually uses in practice.
library(lubridate)
ymd("2026-08-10") # parses a year-month-day formatted string automatically
dmy("10-08-2026") # parses a day-month-year formatted string automatically
mdy("08-10-2026") # parses a month-day-year formatted string automatically
ymd_hms("2026-08-10 14:30:00") # parses a full date and time togetherWhy lubridate's naming is so intuitive
The function names in lubridate directly spell out the order of the components in your text, so
dmy()means โthe text I am giving you is arranged as day, then month, then year.โ This removes the need to remember format code symbols like%dand%mfor the vast majority of everyday parsing tasks.
Extracting and modifying components with lubridate
d <- ymd("2026-08-10")
year(d) # 2026
month(d) # 8
day(d) # 10
wday(d, label = TRUE) # "Mon", the weekday as a readable label
year(d) <- 2027 # you can directly reassign a component, updating the date in placeConvenient duration helpers
d <- ymd("2026-08-10")
d + days(10) # adds exactly 10 days
d + months(2) # adds 2 calendar months, correctly handling different month lengths
d + years(1) # adds 1 year, correctly handling leap years
interval(ymd("2026-01-01"), ymd("2026-08-10")) / days(1) # counts the number of days between two datesmonths() versus a fixed number of days
Adding
months(1)to a date is not the same thing as blindly adding 30 days, since lubridate correctly understands that months have varying lengths. Adding one month to January 31st, for example, correctly rolls into a valid date rather than accidentally landing on a nonexistent day like โFebruary 31st.โ
๐ Where to go next
Dates frequently need to be extracted from messier text first, so pairing this note with R String Manipulation & Regex is common in real projects. Once your data has clean dates attached, R dplyr & Tidyverse and R ggplot2 Visualization make it easy to summarize and plot trends over time.