How do I turn a date into a number representing the count of workdays in that month up to that date?

Viewed 82

I have an array of dates. I need to convert these dates into their representing workday for their respective months. For example, 12/14/2021 (mm/dd/yyyy) must be turned into 10 because it is the 10th workday in the month of December of 2021; similarly, 1/31/2022 must be turned into 21 since it is the 21th workday in the month of January of 2022. If the date is not a workday, we can choose an identifying characters.

I thought about using the bizdays package but I'm struggling here. Could anyone help?

3 Answers

To really get working days in a months you need to account for both weekends and holidays. This is a common problem in finance, so some solutions exist to respect particular exchanges. The QuantLib library has a fairly well known and respected one that covers over 60 exchanges, but it can be difficult to get access as the whole C++ library is a little involved.

I have recently extracted just the calendarding for use with R in a package qlcal that can help here.

For your December example:

> library(qlcal)
> setCalendar("UnitedStates::NYSE")  # New York Stock Exchange
> bd <- getBusinessDays(as.Date("2021-12-01"), as.Date("2021-12-31"))
> data.frame(day=bd, index=seq_along(bd))
          day index
1  2021-12-01     1
2  2021-12-02     2
3  2021-12-03     3
4  2021-12-06     4
5  2021-12-07     5
6  2021-12-08     6
7  2021-12-09     7
8  2021-12-10     8
9  2021-12-13     9
10 2021-12-14    10
11 2021-12-15    11
12 2021-12-16    12
13 2021-12-17    13
14 2021-12-20    14
15 2021-12-21    15
16 2021-12-22    16
17 2021-12-23    17
18 2021-12-27    18
19 2021-12-28    19
20 2021-12-29    20
21 2021-12-30    21
22 2021-12-31    22

and similarly for January

> bd <- getBusinessDays(as.Date("2022-01-01"), as.Date("2022-01-31"))
> data.frame(day=bd, index=seq_along(bd))
          day index
1  2022-01-03     1
2  2022-01-04     2
3  2022-01-05     3
4  2022-01-06     4
5  2022-01-07     5
6  2022-01-10     6
7  2022-01-11     7
8  2022-01-12     8
9  2022-01-13     9
10 2022-01-14    10
11 2022-01-18    11
12 2022-01-19    12
13 2022-01-20    13
14 2022-01-21    14
15 2022-01-24    15
16 2022-01-25    16
17 2022-01-26    17
18 2022-01-27    18
19 2022-01-28    19
20 2022-01-31    20
> 

Assuming we can forget about holidays, then we can use the following function:

business_day <- function(date) {
  vapply(date, function(d) {
    vec <- seq(lubridate::floor_date(d, "month"), d, by = "1 day")
    vec <- lubridate::wday(vec)
    length(which(vec > 1 & vec < 7))
  }, numeric(1))
}

This simply takes the sequence from the start of the month to the given date, and counts up all the days that aren't Saturdays or Sundays. It is vectorized so it can work on any given number of dates. For example:

dates <- as.POSIXct(c("2021-07-26", "2022-02-04", "1999-03-21"))

business_day(dates)
#> [1] 18  4 15

Created on 2022-01-31 by the reprex package (v2.0.1)

First check if date is actually a working day; if it falls on a weekend or into holidays return -1 (do not take a string, otherwise the output is character).

Else, from the seq.Date starting at 01 of the respective month to the respective date, delete weekdays starting with S and those which are in a defined vector h of holidays (if defined).

Actually R is capable of this without using any packages, and much faster.

Accordingly we could define a Vectorized function like this:

f <- Vectorize(\(d, h=NULL) {
  stopifnot(inherits(d, c('Date')))
  stopifnot(inherits(h, c('Date', 'NULL')))
  if (grepl('S', weekdays(d)) | d %in% h) {
    return(-1L)
  } else {
    s <- seq.Date(as.Date(paste0(substr(d, 1, 7), '-01')), d, 1L)
    return(length(s[!grepl('S', weekdays(s)) & !s %in% h]))
  }
 }, vectorize.args='d')

f(dates)
# [1] 10 21 20 -1
f(dates, holydays)
# [1] 10 21 18 -1

Note: R >= 4.1 used.

Note that in R you should always work with the required date format and probably need to convert first:

dates <- as.Date(c("12/14/2021", "01/31/2022"), format='%m/%d/%Y')

If Saturday is a working day, use 'Su' in the grepl, or actually 'Sunday' in your locale, just try weekdays(dates).


Data:

dates <- as.Date(c("2022-12-14", "2022-01-31", "2022-04-28", "2022-12-31"))

holydays <- as.Date(c("2022-01-01", "2022-04-15", "2022-04-18", "2022-05-01",
                      "2022-05-26", "2022-06-06", "2022-08-01", "2022-12-25", 
                      "2022-12-26"))
Related