22

I'm trying to write an R function to calculate the number of weekdays between two dates. For example, something like Nweekdays('01/30/2011','02/04/2011') that would return 5.

Similar to this question.

Update: I was wondering if anyone could think of a way to vectorize this, so that it'll work on 2 columns of dates.

M--
  • 25,431
  • 8
  • 61
  • 93
Zach
  • 29,791
  • 35
  • 142
  • 201

8 Answers8

43
Date1 <- as.Date("2011-01-30")
Date2 <- as.Date("2011-02-04")    
sum(!weekdays(seq(Date1, Date2, "days")) %in% c("Saturday", "Sunday"))

EDIT: This can be vectorized:

Dates1 <- as.Date("2011-01-30") + rep(0, 10)
Dates2 <- as.Date("2011-02-04") + seq(0, 9)
Nweekdays <- Vectorize(function(a, b) 
  sum(!weekdays(seq(a, b, "days")) %in% c("Saturday", "Sunday")))
Nweekdays(Dates1, Dates2)
M--
  • 25,431
  • 8
  • 61
  • 93
J. Win.
  • 6,662
  • 7
  • 34
  • 52
  • Thank you, that's very elegant – Zach Feb 18 '11 at 22:24
  • @J. Winchester Can you think of an easy way to vectorize this function so it works on more than one date at a time? – Zach Feb 18 '11 at 23:32
  • @Zach. Since there is no loop or apply, isn't it already vectorized? You could write the day check into a separate function, then apply that function to all your dates, but that seems like a needless complication. – J. Win. Feb 18 '11 at 23:53
  • @Zach: Never mind, it took me some mental digesting to remember what vectorize means. Edited the answer. – J. Win. Feb 19 '11 at 02:29
  • Will that work in a non-English locale? You need to get the words for Saturday and Sunday in the current locale and check for those. – Spacedman Feb 19 '11 at 11:36
  • Also, in order not to confuse a stupid person, can you put set.seed(1) in any script that uses random numbers for test data! I was wondering why my German result was different to my English! – Spacedman Feb 19 '11 at 11:46
  • @Spacedman: No random numbers now. I guess you can get a vector of your system weekdays like this `weekdays(as.Date("2012-01-01") + 0:6) `; although it would be handy to have something built-in like `month.name`. – J. Win. Feb 19 '11 at 23:15
  • @J. Winchester Man, thanks for introducing me to the "vectorize" function. That will simplify a lot of stuff for me... – Zach Feb 20 '11 at 00:10
  • @J.Won. You might need to remove 1 to your sum if you want to count the number of days between these 2 dates – RockScience May 25 '15 at 04:26
  • @Spacedman In order to solve the problem with locales, you could use lubridate::wday instead of base::weekdays, which gives you a number between 1 and 7, 1 representing Sunday. Instead of "Sunday", you could then write 1. – peer Apr 24 '19 at 11:33
8

These modified functions takes into account of date differences of either positive or negative, whereas the accepted solution accounts for positive date difference.

library("dplyr")

e2 <- structure(list(date.pr = structure(c(16524, 16524, 16524, 16524, 16524, 16524, 16524, 16524, 16524, 16524, 16545, 5974), class = "Date"), 
                     date.po = structure(c(16524, 16525, 16526, 16527, 16528, 16529, 16530, 16531, 16538, 16545, 16524, 15974), class = "Date")), 
                .Names = c("date.1", "date.2"), class = c("tbl_df", "data.frame"), row.names = c(NA, -12L))

1. Locale Dependent Solution: Nweekdays() function is adapted from @J. Won.'s solution. It works for locale = "English_United States.1252"

Nweekdays <- Vectorize(
  function(a, b) 
  {
    ifelse(a < b, 
           return(sum(!weekdays(seq(a, b, "days")) %in% c("Saturday", "Sunday")) - 1), 
           return(sum(!weekdays(seq(b, a, "days")) %in% c("Saturday", "Sunday")) - 1))
  })

a. English Locale

> Sys.setlocale(category="LC_ALL", locale = "English_United States.1252")
[1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"

> Sys.getlocale()
[1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"

> e2 %>%
    mutate(wkd1 = format(date.1, "%A"),
           wkd2 = format(date.2, "%A"),
           ndays_with_wkends = ifelse((date.2 > date.1), (date.2 - date.1), (date.1 - date.2)), 
           ndays_no_wkends = Nweekdays(date.1, date.2))

Source: local data frame [12 x 6]

       date.1     date.2   wkd1      wkd2 ndays_with_wkends ndays_no_wkends
       (date)     (date)  (chr)     (chr)             (dbl)           (dbl)
1  2015-03-30 2015-03-30 Monday    Monday                 0               0
2  2015-03-30 2015-03-31 Monday   Tuesday                 1               1
3  2015-03-30 2015-04-01 Monday Wednesday                 2               2
4  2015-03-30 2015-04-02 Monday  Thursday                 3               3
5  2015-03-30 2015-04-03 Monday    Friday                 4               4
6  2015-03-30 2015-04-04 Monday  Saturday                 5               4
7  2015-03-30 2015-04-05 Monday    Sunday                 6               4
8  2015-03-30 2015-04-06 Monday    Monday                 7               5
9  2015-03-30 2015-04-13 Monday    Monday                14              10
10 2015-03-30 2015-04-20 Monday    Monday                21              15
11 2015-04-20 2015-03-30 Monday    Monday                21              15
12 1986-05-11 2013-09-26 Sunday  Thursday             10000            7143

b. Chinese Locale

> Sys.setlocale(category="LC_ALL", locale = "chinese")
[1] "LC_COLLATE=Chinese (Simplified)_People's Republic of China.936;LC_CTYPE=Chinese (Simplified)_People's Republic of China.936;LC_MONETARY=Chinese (Simplified)_People's Republic of China.936;LC_NUMERIC=C;LC_TIME=Chinese (Simplified)_People's Republic of China.936"

> Sys.getlocale()
[1] "LC_COLLATE=Chinese (Simplified)_People's Republic of China.936;LC_CTYPE=Chinese (Simplified)_People's Republic of China.936;LC_MONETARY=Chinese (Simplified)_People's Republic of China.936;LC_NUMERIC=C;LC_TIME=Chinese (Simplified)_People's Republic of China.936"

> e2 %>%
    mutate(wkd1 = format(date.1, "%A"),
           wkd2 = format(date.2, "%A"),
           ndays_with_wkends = ifelse((date.2 > date.1), (date.2 - date.1), (date.1 - date.2)), 
           ndays_no_wkends = Nweekdays(date.1, date.2))

Source: local data frame [12 x 6]

       date.1     date.2   wkd1   wkd2 ndays_with_wkends ndays_no_wkends
       (date)     (date)  (chr)  (chr)             (dbl)           (dbl)
1  2015-03-30 2015-03-30 ÐÇÆÚÒ» ÐÇÆÚÒ»                 0               0
2  2015-03-30 2015-03-31 ÐÇÆÚÒ» ÐÇÆÚ¶þ                 1               1
3  2015-03-30 2015-04-01 ÐÇÆÚÒ» ÐÇÆÚÈý                 2               2
4  2015-03-30 2015-04-02 ÐÇÆÚÒ» ÐÇÆÚËÄ                 3               3
5  2015-03-30 2015-04-03 ÐÇÆÚÒ» ÐÇÆÚÎå                 4               4
6  2015-03-30 2015-04-04 ÐÇÆÚÒ» ÐÇÆÚÁù                 5               5
7  2015-03-30 2015-04-05 ÐÇÆÚÒ» ÐÇÆÚÈÕ                 6               6
8  2015-03-30 2015-04-06 ÐÇÆÚÒ» ÐÇÆÚÒ»                 7               7
9  2015-03-30 2015-04-13 ÐÇÆÚÒ» ÐÇÆÚÒ»                14              14
10 2015-03-30 2015-04-20 ÐÇÆÚÒ» ÐÇÆÚÒ»                21              21
11 2015-04-20 2015-03-30 ÐÇÆÚÒ» ÐÇÆÚÒ»                21              21
12 1986-05-11 2013-09-26 ÐÇÆÚÈÕ ÐÇÆÚËÄ             10000           10000

2. Locale Independent Solution: Nweekdays() function is adapted from @Sacha Epskamp's solution. It works for all locales, however @Sacha Epskamp used c(0,6) to weed out the weekends, which is different from this solution which uses c(2,3) to extract out weekends.

Nweekdays <- Vectorize(
  function(a, b) {
           return(sum(!(((as.numeric(b:a)) %% 7) %in% c(2,3))) - 1) # 2: Saturday and 3: Sunday
  })

a. English Locale

> Sys.setlocale(category="LC_ALL", locale = "English_United States.1252")
[1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"

> Sys.getlocale()
[1] "LC_COLLATE=English_United States.1252;LC_CTYPE=English_United States.1252;LC_MONETARY=English_United States.1252;LC_NUMERIC=C;LC_TIME=English_United States.1252"

> e2 %>%
    mutate(wkd1 = format(date.1, "%A"),
           wkd2 = format(date.2, "%A"),
           ndays_with_wkends = ifelse((date.2 > date.1), (date.2 - date.1), (date.1 - date.2)), 
           ndays_no_wkends = Nweekdays(date.1, date.2))

Source: local data frame [12 x 6]

       date.1     date.2   wkd1      wkd2 ndays_with_wkends ndays_no_wkends
       (date)     (date)  (chr)     (chr)             (dbl)           (dbl)
1  2015-03-30 2015-03-30 Monday    Monday                 0               0
2  2015-03-30 2015-03-31 Monday   Tuesday                 1               1
3  2015-03-30 2015-04-01 Monday Wednesday                 2               2
4  2015-03-30 2015-04-02 Monday  Thursday                 3               3
5  2015-03-30 2015-04-03 Monday    Friday                 4               4
6  2015-03-30 2015-04-04 Monday  Saturday                 5               4
7  2015-03-30 2015-04-05 Monday    Sunday                 6               4
8  2015-03-30 2015-04-06 Monday    Monday                 7               5
9  2015-03-30 2015-04-13 Monday    Monday                14              10
10 2015-03-30 2015-04-20 Monday    Monday                21              15
11 2015-04-20 2015-03-30 Monday    Monday                21              15
12 1986-05-11 2013-09-26 Sunday  Thursday             10000            7143

b. Chinese Locale

> Sys.setlocale(category="LC_ALL", locale = "chinese")
[1] "LC_COLLATE=Chinese (Simplified)_People's Republic of China.936;LC_CTYPE=Chinese (Simplified)_People's Republic of China.936;LC_MONETARY=Chinese (Simplified)_People's Republic of China.936;LC_NUMERIC=C;LC_TIME=Chinese (Simplified)_People's Republic of China.936"

> Sys.getlocale()
[1] "LC_COLLATE=Chinese (Simplified)_People's Republic of China.936;LC_CTYPE=Chinese (Simplified)_People's Republic of China.936;LC_MONETARY=Chinese (Simplified)_People's Republic of China.936;LC_NUMERIC=C;LC_TIME=Chinese (Simplified)_People's Republic of China.936"

> e2 %>%
    mutate(wkd1 = format(date.1, "%A"),
           wkd2 = format(date.2, "%A"),
           ndays_with_wkends = ifelse((date.2 > date.1), (date.2 - date.1), (date.1 - date.2)), 
           ndays_no_wkends = Nweekdays(date.1, date.2))

Source: local data frame [12 x 6]

       date.1     date.2   wkd1   wkd2 ndays_with_wkends ndays_no_wkends
       (date)     (date)  (chr)  (chr)             (dbl)           (dbl)
1  2015-03-30 2015-03-30 ÐÇÆÚÒ» ÐÇÆÚÒ»                 0               0
2  2015-03-30 2015-03-31 ÐÇÆÚÒ» ÐÇÆÚ¶þ                 1               1
3  2015-03-30 2015-04-01 ÐÇÆÚÒ» ÐÇÆÚÈý                 2               2
4  2015-03-30 2015-04-02 ÐÇÆÚÒ» ÐÇÆÚËÄ                 3               3
5  2015-03-30 2015-04-03 ÐÇÆÚÒ» ÐÇÆÚÎå                 4               4
6  2015-03-30 2015-04-04 ÐÇÆÚÒ» ÐÇÆÚÁù                 5               4
7  2015-03-30 2015-04-05 ÐÇÆÚÒ» ÐÇÆÚÈÕ                 6               4
8  2015-03-30 2015-04-06 ÐÇÆÚÒ» ÐÇÆÚÒ»                 7               5
9  2015-03-30 2015-04-13 ÐÇÆÚÒ» ÐÇÆÚÒ»                14              10
10 2015-03-30 2015-04-20 ÐÇÆÚÒ» ÐÇÆÚÒ»                21              15
11 2015-04-20 2015-03-30 ÐÇÆÚÒ» ÐÇÆÚÒ»                21              15
12 1986-05-11 2013-09-26 ÐÇÆÚÈÕ ÐÇÆÚËÄ             10000            7143
Sathish
  • 12,453
  • 3
  • 41
  • 59
5

I wrote this one, but the other answer is better :)

Nweekdays <- function(a,b)
{
dates <- as.Date(as.Date(a,"%m/%d/%y",origin="1900-01-01"):as.Date(b,"%m/%d/%y",origin="1900-01-01"),origin="1900-01-01")
days <- format(dates,"%w")[c(-1,-length(dates))]
return(sum(!days%in%c(0,6)))
}

Nweekdays('01/30/2011','02/04/2011')
[1] 3

EDIT: Calculates how many weekdays are in between of the two specified days.

Edit:

Taking J. Winchesters advice, the function could be streamlined as:

    Nweekdays <- function(a,b)
{
dates <- as.numeric((as.Date(a,"%m/%d/%y")):(as.Date(b,"%m/%d/%y")))
dates <- dates[- c(1,length(dates))]
return(sum(!dates%%7%in%c(0,6)))
}

Some results:

> Nweekdays('01/30/2011','02/04/2011')
[1] 4
> 
> Nweekdays('01/30/2011','01/30/2011')
[1] 0
> 
> Nweekdays('01/30/2011','01/25/2011')
[1] 3

Note that this is locale independent. (On that topic, how do I change locale anyway?)

Sacha Epskamp
  • 46,463
  • 20
  • 113
  • 131
  • 1
    I started out along those lines, this should be faster than mine if you streamlined it. Converting your date sequence to a `numeric` vector lets you go modulo `%% 7` and drop all the weekend days (eg, `2:3` if your origin is the default). – J. Win. Feb 19 '11 at 00:19
4

Working this out using lubridate you can create a function like:

library(lubridate)

WorkingDays_function <- function(StartDate,EndDate){
startDate <- dmy(StartDate)
endDate <- dmy(EndDate)

#Now build a sequence between the dates:
myDates <-seq(from = startDate, to = endDate, by = "days")


#Week starts on Sunday (1) so to exclude Sun (1) and Sat (7)
#use > 1 & < 7  
working_days <- sum(wday(myDates)>1 & wday(myDates)<7)

print(working_days)
}

WorkingDays_function("11/07/2019","20/07/2019") 
Zoë Turner
  • 459
  • 5
  • 8
2

J. Win.'s answer is good, but can be quite a bit faster with lubridate.

require(lubridate)
count_weekdays<- Vectorize(function(from,to) sum(!wday(seq(from, to, "days")) %in% c(1,7)))

Here are time results from my machine:

> v1<- seq(from = ymd(19000101), to = ymd(20000101), by='month')
> v2<- seq(from = ymd(20000101), to = ymd(21000101), by='month')

> require(tictoc)

> tic(); out<- Nweekdays(v1,v2); toc();
293.06 sec elapsed

> tic(); out<- count_weekdays(v1,v2); toc();
9.95 sec elapsed

About 30x faster. Meaningful if your doing a lot of periods.

Hayward Oblad
  • 709
  • 5
  • 6
1

I use the following approach - first a helper:

weekDays <- function(UPPER = TRUE) {
    days <- c('MONDAY', 'TUESDAY', 'WEDNESDAY',
      'THURSDAY', 'FRIDAY', 'SATURDAY', 
      'SUNDAY')
    if(!UPPER) return(.Internal(tolower(days))) 
    days
}

... and now the main function:

NumWeekDays <- function(dd, Xdays = c('saturday', 'sunday')) {
    # a function to count the number of non-Xdays in a month
    # >
    # first check if Xdays is of correct format
    stopifnot( all(.Internal(tolower(Xdays)) %in% weekDays(UP = FALSE)))
    # >
    # a helper function to find the number of non-X days between two dates
    NonXDays <- function(startDate, endDate, Xdays) {
        sum(!(.Internal(tolower(weekdays(seq(startDate, endDate, 'day')))) %in% 
              .Internal(tolower(Xdays))))
    }
    startDate <- as.Date(as.yearmon(index(dd)), frac = 0)
    endDate <- as.Date(as.yearmon(index(dd)), frac = 1)
    vapply(1:nrow(dd), 
           FUN = function(i) NonXDays(startDate[i], 
                                      endDate[i], 
                                      Xdays = c('saturday', 'sunday')), 
           FUN.VALUE = numeric(1))
}

Example:

set.seed(1)
dx <- apply.monthly(xts(rnorm(600), order.by = Sys.Date() + 1:600), mean)

R> NumWeekDays(dx)
 [1] 23 21 22 23 20 23 22 20 22 22 21 22 23 21 22 22 21 23 21 21
ricardo
  • 8,195
  • 7
  • 47
  • 69
0

This should be faster

wkDaysCnt = function(endDate, startDate) {
    startDays = c("Monday"=5, "Tuesday"=4, "Wednesday"=3, "Thursday"=2, "Friday"=1, "Saturday"=0, "Sunday"=0)
    endDays   = c("Monday"=1, "Tuesday"=2, "Wednesday"=3, "Thursday"=4, "Friday"=5, "Saturday"=5, "Sunday"=5)
    tDays = as.numeric(difftime(endDate, startDate, units="days"))+1
    sdc1 = startDays[weekdays(startDate)]

    sdc2  = ifelse((tDays >= sdc1), sdc1, tDays)
    edc   = endDays[weekdays(endDate)] * (tDays > sdc1+2)

    ret = sdc2 + edc + (5 * round((tDays - sdc2 - edc - 1) / 7) ) - 1
    ret[ret < 0] = 0
    names(ret) = NULL

    return(ret)
}
Ajay
  • 87
  • 4
0

Surprised that bizdays has not been added here (note this returns the number of business days between two dates):

library(bizdays)

# create a calendar 
cal <- create.calendar(
  name = "MyCalendar", 
  weekdays = c("sunday", "saturday"), # non-work days
  holidays = "2023-02-20", # add holidays if you need them
  start.date = "2023-01-01",
  end.date = "2023-12-31"
)

bizdays(from = "2023-02-01", to = "2023-02-28", "MyCalendar")
[1] 18

There are additional options in ?create.calendar if your calendar start and end dates do not fall on business days.

This function is already vectorized:

data.frame(start_date = c("2023-02-01", "2023-05-01"),
           end_date = c("2023-02-28", "2023-05-31")) |>
  dplyr::mutate(biz = bizdays(from = start_date, to = end_date, "MyCalendar"))
#  start_date   end_date biz
#1 2023-02-01 2023-02-28  18
#2 2023-05-01 2023-05-31  22

If you are using the same calendar object repeatedly, you can set it as the default so you do not need to specify it each time in bizdays():

bizdays.options$set(default.calendar = "MyCalendar")
bizdays(from = "2023-02-01", to = "2023-02-28")
[1] 18

bizdays(from = "2023-05-01", to = "2023-05-31")
[1] 22
LMc
  • 12,577
  • 3
  • 31
  • 43