8 Dates

There are several kinds of computation we typically want to do with dates:

  • convert character vectors with date values into dates
  • extract categories of time (year, month, day of the week)
  • calculate elapsed time (differences between dates)
  • increment or decrement dates (a month later, a week earlier)

8.1 Representing Dates

Dates (and times) can be awkward to work with. To begin with, we usually reference points on the calendar (a specific date) with a set of category labels - “year”-“month”-“day”. To compute with these, it is useful to translate them to a number line - each date is a point on one continuous time line. By thinking of calendar dates as points on a line, say \(a\) and \(b\), it becomes clear how they are ordered and how to measure the distance between two points: \(\lvert b-a \rvert\).

However, a second difficulty with dates is that our time units - the category labels “year”, “month”, and “day” - all vary in length. That is, some years have 365 days while others have 366. The length of a month varies from 28 to 31 days. And some days have 23 hours while others have 24 or 25 hours (switching from standard time to daylight savings and back). If two dates are 30 days apart, has more than a “month” passed, exactly a “month”, or not quite a “month”?

8.1.1 The Time Line

In R there are several different ways to solve the dilemmas posed by our measures of dates and times, with different assumptions and constraints. The simplest of these is the Date class (there are also two datetime classes). The Date class translates calendar dates to a time line of integers, where 0 is “1970-01-01”, 1 is “1970-01-02”, -1 is “1969-12-31”, etc. The fundamental unit is one day.

In the following example, we take a date given as a character string and convert it to numeric form. Numeric values with class Date print in a human-readable format. If we coerce a numeric date to a plain numeric class, we can see the underlying number.

x <- "1970-01-01"
y <- as.Date(x)
print(y)
[1] "1970-01-01"
class(y)
[1] "Date"
as.numeric(y)
[1] 0

Today’s date is

Sys.Date()
[1] "2024-03-14"
as.numeric(Sys.Date())
[1] 19796

In other words, today (when this document was last updated) is 19796 days after 1970-01-01.

8.1.2 Date Formats

When converting labeled dates to numeric dates, an initial problem is the huge variety of ways in which we record dates as character strings. You might encounter “2020-11-03” (international standard), “11/03/2020” (a typical American representation), or even “November 3, 2020” (another typical American representation), all of which label the same point on the calendar.

The international standard is the R default, so it needs no special handling. Typical American date representations require you to specify a format to make the conversion to a Date.

In this context, a format is a character string that specifies the template for reading dates. We can see help(strptime) to find formatting codes, which start with %. For example, %m is “Month as decimal number (01–12)”m while %B is “Full month name in the current locale.” Between these codes, we can use spaces, slashes, commas, or whatever other symbols are used in the dates. If our dates follow the default format of YYYY-MM-DD, we do not need a format code.

as.Date("2020-11-03") # default format, %Y-%m-%d
[1] "2020-11-03"
as.Date("11/03/2020", format = "%m/%d/%Y")
[1] "2020-11-03"
as.Date("November 3, 2020", format = "%B %e, %Y")
[1] "2020-11-03"

Taking another look at the final line above, notice that the separators (here, spaces and a comma) are included when specifying the format. %B is a complete month name (November), %e is a day of the month (3) preceded by a space and followed by a comma and a space, and %Y is a four-digit year (2020).

Specifying the format manually as we just did gives us control over exactly how dates are to be interpreted. We can also have R parse the date string for us by ordering y, m, and d into a function name, such as mdy() for month-day-year dates. For this approach, the formats are allowed to vary within our vector. If R cannot figure out the date, it will return NA. Just be sure that it does not try to interpret anything unexpected in the data! To use these y-m-d functions, load the lubridate package first.

library(lubridate)

mdy(c("11/03/2020", "November 3, 2020", "11032020"))
[1] "2020-11-03" "2020-11-03" "2020-11-03"

mdy(c("feb 29 2021", "hello", "2020-11-03"))
Warning: 3 failed to parse.
[1] NA NA NA

ymd("2020-11-03")
[1] "2020-11-03"

In the first set of dates, notice that we can supply one of our date parsers with multiple formats. In the second set, see how all three dates simply return NA, but for different reasons - February 29 does not exist in 2021, “hello” is clearly not a date, and “2020-11-03” is ymd and not mdy. This last value is correctly handled by ymd() in the third example.

8.2 Extracting Date Categories

The same formats are used when we want to extract category labels - months or years - from a Date. We use the strftime() function to convert from a numeric Date to a category label.

In this example we extract the year part of several dates.

dates  <- c("04/10/1964", "06/18/1965", "09/21/1966")
ndates <- as.Date(dates, format="%m/%d/%Y")
strftime(ndates, format="%Y")
[1] "1964" "1965" "1966"

Notice that these are returned as character values!

There a several ways we might label months: with a full name, with an abbreviated name, or with a numeral. Each of these has its own format code.

strftime(ndates, format="%b")
[1] "Apr" "Jun" "Sep"
strftime(ndates, format="%m")
[1] "04" "06" "09"

Again, the result is a vector of character values.

lubridate gives us the option of extracting numeric values rather than character values with aptly named functions such as year(), month(), day(), and quarter(). Beyond these, we can extract the day of the year (yday()), quarter (qday()), and week (wday(), where 1 is Monday), as well as the week of the year (week()). For even more, see help(day).

year(ndates)
[1] 1964 1965 1966
month(ndates)
[1] 4 6 9
day(ndates)
[1] 10 18 21
quarter(ndates)
[1] 2 2 3
yday(ndates)
[1] 101 169 264
qday(ndates)
[1] 10 79 83
wday(ndates)
[1] 6 6 4
week(ndates)
[1] 15 25 38

8.3 Elapsed Time

Storing dates as numeric values makes it easy to compute elapsed times: you just subtract one date from another. The difference is the number of days that have passed.

How many days have passed since January 1, 2000?

daysgoneby <- Sys.Date() - as.Date("2000-01-01") 
daysgoneby
Time difference of 8839 days

The result is numeric data, but with a new class, difftime. The largest time unit supported by difftimes is days (actually, weeks, but these are just seven days), since the larger units (months and years) vary in length. Sometimes, we have a good reason for dealing with these ambiguous units, such as when we want to calculate ages from birth dates.

To do this, we should use objects with class Interval rather than difftime, and pass these objects to lubridate’s time_length() function. When we give intervals to time_length(), it will account for varying month and year lengths and give us the results we would expect, whereas with difftimes, it will assume years are all 365.25 days and all months are 30.4375 (365.25/12) days long.

We can give two dates to interval() function, and then pass the result to time_length() and specify unit = "years" or unit = "months". Note that interval() calculates the difference as the second date minus the first date, rather than the second date minus the first date as with difftime(), so the sign on the result is reversed if the order is the same.

The first example uses a “regular” non-leap year with 365 days. difftime() returns a difference of -365/365.25 = -0.999 years, while interval() returns 1. In the second example with a leap year, difftime() gives us an answer slightly over 1 (366/365.25) while interval() still calculates it as one year.

time_length(difftime(as.Date("2019-01-01"), as.Date("2020-01-01")), unit = "years")
[1] -0.9993155
time_length(interval(as.Date("2019-01-01"), as.Date("2020-01-01")), unit = "years")
[1] 1
time_length(difftime(as.Date("2020-01-01"), as.Date("2021-01-01")), unit = "years")
[1] -1.002053
time_length(interval(as.Date("2020-01-01"), as.Date("2021-01-01")), unit = "years")
[1] 1

This means that, if we are working with whole dates (no hours, minutes, seconds, etc.) and single years, time_length() will never return a whole number when working with a difftime() object.

The same is true of months, since time_length() will assume 30.4375 days per month with difftimes. We can observe this if we pass months of 28, 29, 30, and 31 days to either difftime() or interval() when calculating the time_length() in months:

df <- data.frame(start_date = ymd(20210201, 20200201, 20200401, 20210301),
                 end_date = ymd(20210301, 20200301, 20200501, 20210401))

df$n_days <- as.numeric(df$end_date - df$start_date)

df$length_difftime <- time_length(difftime(df$end_date, df$start_date), unit = "months")
df$length_interval <- time_length(interval(df$start_date, df$end_date), unit = "months")
df
  start_date   end_date n_days length_difftime length_interval
1 2021-02-01 2021-03-01     28       0.9199179               1
2 2020-02-01 2020-03-01     29       0.9527721               1
3 2020-04-01 2020-05-01     30       0.9856263               1
4 2021-03-01 2021-04-01     31       1.0184805               1

8.4 Incrementing and Decrementing Dates

Another limitation of the Date class is that incrementing or decrementing by units greater than days is awkward - again the ambiguity of months and years is an obstacle.

Suppose we wanted to increment some dates by one month. We could try

dates <- as.Date(c("2004-02-10", "2005-06-18", "2007-07-21"))
dates + 30
[1] "2004-03-11" "2005-07-18" "2007-08-20"

The first and third values here are probably not what we had in mind!

We usually think of retaining the same day, but incrementing (or decrementing) the month category. This can be accomplished with lubridate’s add_with_rollback() function. Provide the function with a date and a period (a pluralized date component: years(), months(), weeks(), or days()).

We can add one month to each date in our dates vector with months(1):

add_with_rollback(dates, months(1))
[1] "2004-03-10" "2005-07-18" "2007-08-21"

Giving a negative number to the period function allows us to subtract that period from the date:

add_with_rollback(dates, years(-1))
[1] "2003-02-10" "2004-06-18" "2006-07-21"
add_with_rollback(dates, months(-2))
[1] "2003-12-10" "2005-04-18" "2007-05-21"
add_with_rollback(dates, weeks(-3))
[1] "2004-01-20" "2005-05-28" "2007-06-30"
add_with_rollback(dates, days(-4))
[1] "2004-02-06" "2005-06-14" "2007-07-17"

When adding or subtracting months or years to dates, we are forced to deal with uneven month lengths. What is one month after January 31? What is one year after February 29?

As the function name suggests, add_with_rollback() will subtract days until the date is legitimate. Adding one month to January 31 will return the last day of February, and adding one year to Febuary 29 will result in February 28 of the following year.

add_with_rollback(ymd(20210131), months(1))
[1] "2021-02-28"
add_with_rollback(ymd(20200229), years(1))
[1] "2021-02-28"

If we want to instead end up with March 1 in either case above, the first day of the next month, add the argument roll_to_first = TRUE, which is FALSE by default.

add_with_rollback(ymd(20210131), months(1), roll_to_first = TRUE)
[1] "2021-03-01"
add_with_rollback(ymd(20200229), years(1), roll_to_first = TRUE)
[1] "2021-03-01"

8.5 Exercises

  1. Date formats: Other software uses other conventions for labeling date values. SAS and Stata both print dates as “10apr2004” by default. Convert the following SAS/Stata dates to R Dates:

    10apr2004
    18jun2005
    21sep2006
    12jan2007
  2. Extracting date categories: Using the extract vector of dates below, extract the years, months, days, and days of the week. How many are Wednesdays?

    extract <- ymd("2013-06-11", "2015-03-10", "2017-08-13", "2011-05-29", "2010-12-13")
  3. Elapsed time: Calculate your age in years, months, and days, as of today (use Sys.Date()). Be sure to account for irregular month and year lengths.

  4. Selecting data based on a date cutoff: Given the following vector x, create an indicator showing which observations occur on or after July 1 (whether they fall in fiscal year 2021). How many of these observations are there? (The set.seed() function makes it so that if we give the same number to the function, we will produce the same random numbers for x.)

    set.seed(112)
    x <- as.Date(sample(1:365, 10), 
                 origin="2020-01-01")

8.6 Advanced Exercises

  1. Average and standard deviation of dates: Using the dates from the first exercise, calculate an average date. What class is the returned value? Calculate the standard deviation. What class is this? Why should the mean and standard deviation return values of different classes?

  2. Dates from date components: Occasionally you will work with data where the month, day, and year components of dates are stored as separate variables. To convert these to dates, first paste them together. (Recall that, to reference a column in a dataframe, use $, as in df$day.)

    df <- data.frame(day = c(10, 18, 22),
                     month = c(4, 6, 9),
                     year  = c(2004, 2005, 2006))
  3. Creating dates from integers: In the exercise using sample() above, R converts random integers into dates, provided that we specify the origin date. Most often this will be the same as the origin for Date values, “1970-01-01”.

    • Convert the integers 0:5 to R dates, assuming the usual R origin.

    • Other software use other origins for their timelines. Date values in SAS and Stata use 01jan60 as their origin. Now assume the integers 0:5 are SAS/Stata date values, using their default origin. Then convert these to R dates. What values do they take?

  4. Extracting day of the week: Using strftime() to get the day of the week (Sunday, Monday, etc.) for each observation of this vector from earlier:

    set.seed(112)
    x <- as.Date(sample(1:365, 10), 
                 origin="1970-01-01")