30  Date and Time

R includes support for working with date and time data. There are three builtin classes:

POSIXct is more efficient and faster than POSIXlt and therefore is the recommended structure for date-time data. At the same time, POSIXlt can be convenient for extracting individual components using the $ operator.

The data.table package includes the following classes:

data.table’s fread() can automatically detect some date and date-time formats and will convert them to IDate and POSIXct respectively.

30.1 Date format specifications

This list serves as a reference for formatting functions later in the chapter.

  • %Y: Year with century, (0-9999 accepted) e.g. 2020
  • %y: 2-digit year, e.g. 22
  • %m: Month, 01-12, e.g. 03
  • %d: Day, 01-31, e.g. 04
  • %H: Hours, 00-23, e.g. 13
  • %I: Hours, 01-12, e.g. 01
  • %M: Minutes, 00-59, e.g. 38
  • %S: Seconds, 00-61 (sic!) allowing for up to two leap seconds, e.g. 54

There are many more specifications available, see the Details section in the documentation for strptime().

Note that some conversions are locale-specific, i.e. will not work the same across systems.

Regarding the ambiguous %y, the documentation states: “%y Year without century (00–99). On input, values 00 to 68 are prefixed by 20 and 69 to 99 by 19 – that is the behaviour specified by the 2018 POSIX standard, but it does also say ‘it is expected that in a future version the default century inferred from a 2-digit year will change’.”

30.2 POSIX standard

Portable Operating System Interface (POSIX) is a set of standards for maintaining compatibility among operating systems. We recommend using the POSIX standard for date, "%Y-%m-%d", and date-time, "%Y-%m-%d %H:%M:%S", formats whenever possible.

30.3 Date objects

30.3.1 Character to Date: as.Date()

You can create a Date object from a character using as.Date():

x <- as.Date("1981-02-12")
x
[1] "1981-02-12"
[1] "Date"

The tryFormats argument defines which format(s) are recognized.

The default is tryFormats = c("%Y-%m-%d", "%Y/%m/%d"), i.e. will recognize dates of the form “2020-11-16” or “2020/11/16”.

Let’s see what happens if a date format is not recognized. Consider the 16th of November, 2021 written as follows:

z <- "11.09.21"

When passed to as.Date(), it will result to an error, because it is not in a recognized format:

zt <- as.Date(z)
Error in charToDate(x): character string is not in a standard unambiguous format

Defining format will allow the date to be read correctly:

zt <- as.Date(z, tryFormats = "%m.%d.%y")
zt
[1] "2021-11-09"

You can convert to data.table’s IDate class using as.IDate():

library(data.table)
xi <- as.IDate("1981-02-12")
xi
[1] "1981-02-12"
class(xi)
[1] "IDate" "Date" 

as.IDate() supports extra arguments just like base as.Date(). Using the same example as above, we can specify the same tryFormats argument:

zi <- as.IDate(z, tryFormats = "%m.%d.%y")
zi
[1] "2021-11-16"
class(zi)
[1] "IDate" "Date" 

It’s always safest to specify the format of your date string explicitly.

30.3.2 Get current date or date & time

Get current date:

today <- Sys.Date()
today
[1] "2023-11-16"
class(today)
[1] "Date"

Get current date and time:

now <- Sys.time()
now
[1] "2023-11-16 02:47:14 PST"
class(now)
[1] "POSIXct" "POSIXt" 

Get local timezone:

[1] "America/Los_Angeles"

30.3.3 Math on Dates

The reason we use special date and date-time classes is because they allow us to perform mathematical operations on them.

For example, we can subtract date objects to get time intervals:

start_date <- as.Date("2020-09-15")
time_diff <- Sys.Date() - start_date
time_diff
Time difference of 1157 days
class(time_diff)
[1] "difftime"

Note: While you can use the subtraction operator -, it is advised you use the difftime() function to perform subtraction on dates instead, because it allows you to specify units.

timepoint1 <- as.Date("2020-01-07")
timepoint2 <- as.Date("2020-02-03")
difftime(timepoint2, timepoint1, units = "weeks")
Time difference of 3.857143 weeks
difftime(timepoint2, timepoint1, units = "days")
Time difference of 27 days
difftime(timepoint2, timepoint1, units = "hours")
Time difference of 648 hours
difftime(timepoint2, timepoint1, units = "mins")
Time difference of 38880 mins
difftime(timepoint2, timepoint1, units = "secs")
Time difference of 2332800 secs

Why is there no option for “months” or “years” in units?

Unlike seconds, minutes, hours, days, and weeks, months and years do not have fixed length. Months can contain 28-31 days and years can contain 365 or 366 days and therefore cannot be used as units of time.

You can always get a difference in days and divide by 365.2422 or some other approximation.

DOB <- as.Date("1969-08-04")
Age <- difftime(Sys.Date(), DOB, units = "days")
Age
Time difference of 19827 days

difftime() outputs objects of class difftime:

class(Age)
[1] "difftime"

If you convert the output of difftime() using an appropriate mathematical operation, e.g. division, the units will remain, even though they are no longer correct. Use as.numeric() to convert the difftime object to a regular numeric vector and remove the units which are no longer valid.

Note that the units (in this case, “days”) remain after a mathematical operation. For example, if you wanted to convert the age to years, you would want to avoid this:

Age_years <- Age / 365.2422
Age_years
Time difference of 54.28453 days

The output is still of class difftime with units “days”.

class(Age_years)
[1] "difftime"

Instead, eliminate the units by converting the difftime object to numeric:

Age_years <- as.numeric(Age) / 365.2422
Age_years
[1] 54.28453
class(Age_years)
[1] "numeric"

30.3.4 mean/median Date

x <- as.Date(c(5480, 5723, 5987, 6992), origin = "1970-01-01")
x
[1] "1985-01-02" "1985-09-02" "1986-05-24" "1989-02-22"
mean_date <- mean(x)
mean_date
[1] "1986-07-21"
class(mean_date)
[1] "Date"
median_date <- median(x)
median_date
[1] "1986-01-12"
class(median_date)
[1] "Date"

To verify the median, we can do a mathematical operations using multiplication, subtraction, and addition, and the result is still a Date(!):

median_date_too <- x[2] + 0.5 * (x[3] - x[2])

30.3.5 Sequence of dates

We have previously used seq() to create numeric sequences. When applied on Date objects, it will create sequences of dates. Note that when seq() is applied on a Date object and an integer is passed to the by argument, the unit is assumed to be days:

start_date <- as.Date("2020-09-14")
end_date <- as.Date("2020-12-07")
seq(from = start_date, to = end_date, by = 7)
 [1] "2020-09-14" "2020-09-21" "2020-09-28" "2020-10-05" "2020-10-12"
 [6] "2020-10-19" "2020-10-26" "2020-11-02" "2020-11-09" "2020-11-16"
[11] "2020-11-23" "2020-11-30" "2020-12-07"

Conveniently, unlike mathematical operations like difftime() which require strict units of time, seq() can work with months and years.

Argument by can also be one of:

“day”, “week”, “month”, “quarter”, “year”, or combination of an integer and one of these strings:

“3 days”, “2 weeks”, “6 months”, “4 years”, etc.

Therefore, by = 7 is equivalent to by = "7 days".

seq(from = start_date, to = end_date, by = "7 days")
 [1] "2020-09-14" "2020-09-21" "2020-09-28" "2020-10-05" "2020-10-12"
 [6] "2020-10-19" "2020-10-26" "2020-11-02" "2020-11-09" "2020-11-16"
[11] "2020-11-23" "2020-11-30" "2020-12-07"

and also to by = week or by = "1 week":

seq(from = start_date, to = end_date, by = "week")
 [1] "2020-09-14" "2020-09-21" "2020-09-28" "2020-10-05" "2020-10-12"
 [6] "2020-10-19" "2020-10-26" "2020-11-02" "2020-11-09" "2020-11-16"
[11] "2020-11-23" "2020-11-30" "2020-12-07"

As another example, create a sequence of dates every 2 months:

start_date <- as.Date("2020-01-20")
end_date <- as.Date("2021-01-20")
seq(start_date, end_date, by = "2 months")
[1] "2020-01-20" "2020-03-20" "2020-05-20" "2020-07-20" "2020-09-20"
[6] "2020-11-20" "2021-01-20"

As with numeric sequences, you can also define the length.out argument:

start_date <- as.Date("2020-01-20")
seq(from = start_date, by = "year", length.out = 4)
[1] "2020-01-20" "2021-01-20" "2022-01-20" "2023-01-20"

30.4 Date-Time objects

POSIXct and POSIXlt are two classes for representing date-time objects in R.

30.4.1 Character to Date-Time: as.POSIXct() & as.POSIXlt():

We start by creating a character string representing date and time:

dt <- "2020-03-04 13:38:54"
dt
[1] "2020-03-04 13:38:54"
class(dt)
[1] "character"

We can then use as.POSIXct() and as.POSIXlt() to convert the character string to POSIXct and POSIXlt date-time objects, respectively:

dt_posixct <- as.POSIXct(dt)
dt_posixct
[1] "2020-03-04 13:38:54 PST"
class(dt_posixct)
[1] "POSIXct" "POSIXt" 
str(dt_posixct)
 POSIXct[1:1], format: "2020-03-04 13:38:54"

POSIXct stores date-time information as the number of seconds since January 1, 1970. This becomes apparent when converting the object to numeric.

as.numeric(dt_posixct)
[1] 1583357934
dt_posixlt <- as.POSIXlt(dt)
dt_posixlt
[1] "2020-03-04 13:38:54 PST"
class(dt_posixlt)
[1] "POSIXlt" "POSIXt" 
str(dt_posixlt)
 POSIXlt[1:1], format: "2020-03-04 13:38:54"
dt_posixlt$year
[1] 120

POSIXlt stores date-time information as a named list with components for year, month, day, hour, minute, second, etc. The year component is stored as the number of years since 1900. Confusingly, converting the object to numeric will give the number of seconds since January 1, 1970, just like POSIXct.

dt_posixlt$year
[1] 120
dt_posixlt$year + 1900
[1] 2020
as.numeric(dt_posixlt)
[1] 1583357934

You can use attributes() to see the difference between the POSIXct and POSIXlt classes:

attributes(dt_posixct)
$class
[1] "POSIXct" "POSIXt" 

$tzone
[1] ""
attributes(dt_posixlt)
$names
 [1] "sec"    "min"    "hour"   "mday"   "mon"    "year"   "wday"   "yday"  
 [9] "isdst"  "zone"   "gmtoff"

$class
[1] "POSIXlt" "POSIXt" 

$tzone
[1] ""    "PST" "PDT"

$balanced
[1] TRUE

One of the advantages of POSIXlt is that you can access individual components using the $ operator:

dt_posixlt$year + 1900  # year, stored as years since 1900
[1] 2020
dt_posixlt$mon  # month is 0-indexed (0 = January, ..., 11 = December)
[1] 2
dt_posixlt$mday # day of month
[1] 4
dt_posixlt$hour # hour
[1] 13
dt_posixlt$min  # minute
[1] 38
dt_posixlt$sec  # second
[1] 54

Both functions feature a format argument, which defines the order and format of characters to be read as year, month, day, hour, minute, and second information.

You can learn more about the format syntax by reading strptime()’s documentation.

For example, the international ISO 8601 standard is defined as "%Y-%m-%d %H:%M:%S" .

You can use any combination of specifications to match your data. For example, consider the following date-time string:

dt2 <- c("03.04.20 01:38.54 pm")

The correct format for this would be as follows:

dt2_posix <- as.POSIXct(dt2, format = "%m.%d.%y %I:%M.%S %p")
dt2_posix
[1] "2020-03-04 13:38:54 PST"

30.5 format() Date

format() operates on Date and POSIX objects to convert between representations

Define Date in US format:

dt_us <- as.Date("07-04-2020", format = "%m-%d-%Y")
dt_us
[1] "2020-07-04"

Convert to European format (i.e. day followed by month):

dt_eu <- format(dt_us, "%d.%m.%y")
dt_eu
[1] "04.07.20"

30.6 format() POSIXct

dt <- as.POSIXct("2020-03-04 13:38:54")
dt
[1] "2020-03-04 13:38:54 PST"
format(dt, "%m/%d/%Y @ %H:%M:%S")
[1] "03/04/2020 @ 13:38:54"

To get the relevant R documentation pages, use ?format.Date and ?format.POSIXct. To learn more about S3 classes and methods, see Chapter 32.

30.7 Extract partial date information

R includes convenient functions to extract particular seasonal information

  • weekdays(): Get name of day of the week
  • months(): Get name of month
  • quarters(): Get quarter
  • julian(): Get number of days since a specific origin
x <- as.Date(c(18266, 18299, 18359, 18465), origin = "1970-01-01")
x
[1] "2020-01-05" "2020-02-07" "2020-04-07" "2020-07-22"
[1] "Sunday"    "Friday"    "Tuesday"   "Wednesday"
[1] "January"  "February" "April"    "July"    
[1] "Q1" "Q1" "Q2" "Q3"
[1] 18266 18299 18359 18465
attr(,"origin")
[1] "1970-01-01"
julian(x, origin = as.Date("2020-01-01"))
[1]   4  37  97 203
attr(,"origin")
[1] "2020-01-01"

30.8 Timezones

R supports timezones. You can see the current timezone using Sys.timezone():

[1] "America/Los_Angeles"

Get current date and time:

now <- Sys.time()
now
[1] "2025-10-21 19:31:48 PDT"

You can get a list of all available timezones using OlsonNames(). There are 597 available timezones. Let’s print the first 20:

OlsonNames()[1:20]
 [1] "Africa/Abidjan"       "Africa/Accra"         "Africa/Addis_Ababa"  
 [4] "Africa/Algiers"       "Africa/Asmara"        "Africa/Asmera"       
 [7] "Africa/Bamako"        "Africa/Bangui"        "Africa/Banjul"       
[10] "Africa/Bissau"        "Africa/Blantyre"      "Africa/Brazzaville"  
[13] "Africa/Bujumbura"     "Africa/Cairo"         "Africa/Casablanca"   
[16] "Africa/Ceuta"         "Africa/Conakry"       "Africa/Dakar"        
[19] "Africa/Dar_es_Salaam" "Africa/Djibouti"     

You can use Sys.setenv() to set the timezone temporarily within an R session:

Sys.setenv(TZ = "America/New_York")

Note how setting the timezone changes how the date and time are printed:

now
[1] "2025-10-21 22:31:48 EDT"

Setting the timezone using Sys.setenv() affects the current R session. To set the timezone permanently, you need to set the TZ environment variable in your operating system.

Use format()’s tz argument to convert date & time to different timezones.

format(now, tz = "Europe/London")
[1] "2025-10-22 03:31:48"
format(now, tz = "Africa/Nairobi")
[1] "2025-10-22 05:31:48"
format(now, tz = "Asia/Tokyo")
[1] "2025-10-22 11:31:48"
format(now, tz = "Australia/Sydney")
[1] "2025-10-22 13:31:48"

Note that the output of format() is not POSIXct or POSIXlt, but a character:

class(now)
[1] "POSIXct" "POSIXt" 
class(format(now, tz = "Europe/London"))
[1] "character"

The timezone is not part of the POSIXct object itself:

$class
[1] "POSIXct" "POSIXt" 

The print() method for datetime objects calls format() internally.

30.9 Mixing Date and Date-Time objects

Do not perform mathematical operations between Date and POSIXct/lt objects. You may get a warning (not an error) and incorrect results.

x_date <- as.Date("2020-11-16")
y_datetime <- as.POSIXct("2020-11-18 15:30:00")
x_date
[1] "2020-11-16"
y_datetime
[1] "2020-11-18 15:30:00 EST"

The following produces a warning and an incorrect result:

y_datetime - x_date
Warning: Incompatible methods ("-.POSIXt", "-.Date") for "-"
[1] "2020-11-18 10:20:18 EST"

In this case, you could convert the POSIXct object to Date first, effectively stripping the time information:

as.Date(y_datetime) - x_date
Time difference of 2 days

Note that if you convert the Date object to POSIXct first, the time will be set to midnight (00:00:00), which may not be what you want:

y_datetime - as.POSIXct(x_date)
Time difference of 2.854167 days

30.10 See also

© 2025 E.D. Gennatas