๐Ÿ“… 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 object

Date 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, 2026

Common format codes

CodeMeaningExample
%Y4 digit year2026
%y2 digit year26
%m2 digit month08
%d2 digit day10
%BFull month nameAugust
%bAbbreviated month nameAug
%AFull weekday nameMonday
%HHour, 24 hour format14
%MMinute30
%SSecond00

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 tz argument 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 together

Why 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 %d and %m for 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 place

Convenient 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 dates

months() 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.