Menu

Dates in R: as.Date, format() and Date Arithmetic

R's Date class is a day count wearing a costume. How to parse strings with as.Date, format dates for output, subtract them, build sequences, and when you need POSIXct instead.

This page includes runnable editors - edit, run, and see output instantly.

A Date in R Is a Day Count Wearing a Costume

To turn a string into a real date, use as.Date(); to get today, use Sys.Date(). Behind both is a simple trick: R's Date class is just a number - the count of days since January 1, 1970 - with a costume on that makes it print like a calendar date. You can pull the costume off with unclass():

That 20672 is the raw day count, and it's what makes everything else on this page work: comparisons, subtraction, and sequences are all just integer math underneath. The costume is what makes it usable - the same number prints as a date, formats as a date, and plots on a date axis.

as.Date() happily parses the ISO layout "YYYY-MM-DD" with no help, which is one of several good reasons to store dates that way in your files. One habit to build early: a failed parse returns NA silently - not an error - so always glance at the result after parsing something new.

Strings that merely look like dates are still just strings: "2026-08-07" < "2026-11-01" happens to compare correctly because ISO order matches alphabetical order, but "07/08/2026" sorts as nonsense. Convert first, then compare.

Parsing Other Layouts With format =

The world does not write ISO dates. For everything else, as.Date() takes a format = argument: a recipe describing the incoming string, built from % codes.

All three parse to the same day. The recipe must match the string exactly, separators included: "%d/%m/%Y" will not parse "07-08-2026". The codes you'll actually use:

CodeMeaningExample
%Y4-digit year2026
%y2-digit year26
%mMonth number08
%dDay of month07
%bAbbreviated month nameAug
%BFull month nameAugust
%aAbbreviated weekdayFri
%AFull weekdayFriday
%HHour (00-23)14
%MMinute30
%SSecond05

The classic ambush is "07/08/2026" itself: day/month/year in most of the world, month/day/year in the US, and R cannot guess which one your file means. Worse, a wrong guess often "works" - both readings are valid dates - until day 13 of some month arrives and parses to NA (there is no 13th month). Know your data's convention before you write the recipe.

Formatting Dates for Output

format() is as.Date()'s mirror image: the same % codes, running the opposite direction - Date in, string out.

Use this for report titles, labels, and filenames - format(Sys.Date(), "%Y-%m-%d") makes names that sort chronologically. Two details worth knowing: %d zero-pads ("August 07"), and %B/%A produce names in your system's language settings - a session configured for German prints "Freitag" instead of "Friday". Keep formatting at the edges of your script, for humans; internally, leave dates as Dates.

Date Arithmetic and Sequences

Because a Date is a day count, arithmetic works the way you'd hope. Subtracting two dates gives a difftime object:

Three habits to take from that block. First, wrap a difftime in as.numeric() before doing math with it - difftime carries a units attribute that can surprise later calculations. Second, when the units matter, state them with difftime(units = ) instead of accepting whatever R picks. Third, adding a plain number means adding days - there is no + 1 month in base R, because "one month" isn't a fixed number of days.

For calendar-aware stepping, use seq(), which understands "day", "week", "month", "quarter" and "year":

Month sequences are the standard tool for report periods and axis breaks. One quirk: start a monthly sequence on the 31st and R keeps counting real days through months that lack one, so the "February" entry lands in early March. Anchor monthly sequences on the 1st and derive anything else from there.

Extracting Parts of a Date

Base R has two convenience extractors and one general idiom. weekdays() and months() return names; for numbers, round-trip through format():

The format()-then-as.integer() move looks roundabout, but it's the standard base-R idiom, and it's vectorized - hand it a whole column of dates and you get a whole column of years back. That's exactly what you want after importing a dated dataset from a CSV file: parse the date column once with as.Date(), then derive year and month columns for grouping.

When Days Aren't Enough: POSIXct, and lubridate

Date has no concept of hours. The moment your data has timestamps - log entries, sensor readings, "order placed at 14:32" - you need POSIXct, R's date-time class: seconds since 1970 instead of days, and time-zone aware.

Everything transfers: the same % codes (now including %H, %M, %S) work for parsing and format(), subtraction gives difftimes, Sys.time() is "now". The new burden is the time zone - pass tz = explicitly when parsing, because two sessions in different zones will otherwise read the same string as two different instants. And resist storing calendar data as POSIXct "just in case": a birthday stored as midnight-in-some-zone can print as the previous day in another zone, a genuinely miserable bug to trace. Whole days: Date. Instants: POSIXct.

Finally, the lubridate package is the friendlier face most tidyverse code puts on all of the above - worth adopting once the base mechanics make sense:

library(lubridate)

ymd("2026-08-07")            # order-based parsing: no format string
mdy("08/07/2026")            # ...you just say which order the parts come in
dmy("7 August 2026")
floor_date(Sys.Date(), "month")   # snap to the start of the period

ymd(), mdy() and dmy() replace format recipes with a simple statement of part order, and floor_date() solves "group these timestamps by month" in one call. lubridate rides on the same Date/POSIXct classes underneath - a better steering wheel, not a different engine.

What You Take Away

  • A Date is a day count since 1970-01-01 dressed up to print like a date; unclass() shows the number.
  • as.Date("2026-08-07") parses ISO directly; anything else needs a format = recipe of % codes - and a failed parse returns NA, silently.
  • format(date, "...") is the reverse: the same codes, producing display strings.
  • Subtraction gives a difftime - set units = explicitly, as.numeric() before doing math. + n adds days; for months, use seq(by = "month").
  • weekdays(), months(), and the as.integer(format(...)) idiom pull out parts, vectorized.
  • Date for whole days, POSIXct (with an explicit tz) for timestamps; lubridate makes both friendlier.

Next up: what to do when R shouts at you - reading error messages and debugging them methodically.

Frequently Asked Questions

How do you convert a string to a date in R?

With as.Date(). If the string is in ISO format it just works: as.Date("2026-08-07"). Any other layout needs a format = recipe built from % codes: as.Date("07/08/2026", format = "%d/%m/%Y"). A failed parse returns NA rather than an error, so check the result.

How do you get today's date in R?

Sys.Date() returns today as a Date object; Sys.time() returns the current date-time as a POSIXct. Both come from your computer's clock and time zone settings.

How do you calculate the difference between two dates in R?

Subtract them: as.Date("2026-08-07") - as.Date("2026-01-01") gives a difftime of 218 days. Wrap it in as.numeric() to use the number in math, or call difftime(d1, d2, units = "weeks") to choose the units yourself instead of letting R pick.

What is the difference between Date and POSIXct in R?

Date stores whole days (internally: days since 1970-01-01) and knows nothing about hours or time zones. POSIXct stores an instant in time (seconds since 1970) and is time-zone aware. Use Date for calendar data like birthdays and deadlines; use POSIXct only when the time of day matters, like timestamps and logs.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED