Boxuan
Boxuan

Reputation: 5157

Determine if a weekday is between two dates in R

[Update]

In another thread, the answer from @Frank solves the problem. This question becomes duplicate of the other.


[Question]

I am writing a function in R to test if a weekday is in between two dates. Here is what I have, but I think the solution is not elegant. Is there a more mathematical way to do it?

library(data.table) ## wday is a function in this package
isDayIn <- function(weekday, date1, date2) {
  if (weekday<1 | weekday>7) stop("weekday must be an integer from 1 to 7.")
  date1 <- as.Date(date1)
  date2 <- as.Date(date2)
  output <- weekday %in% unique(wday(seq.Date(date1, date2, by=1)))
  return(output)
}

## 2015-08-02 is a Sunday and 2015-08-03 is a Monday
isDayIn(1, "2015-08-02", "2015-08-03")
> TRUE
isDayIn(7, "2015-08-02", "2015-08-03")
> FALSE

Note: the function wday starts on Sunday and ends on Saturday, so Sunday will be mapped to integer 1 and Saturday will be mapped to integer 7.

Upvotes: 2

Views: 981

Answers (4)

Jonas Lindel&#248;v
Jonas Lindel&#248;v

Reputation: 5683

I wrote a vectorized version of the answer by @CL that is also slightly more general:

#' Check if a weekday is within an interval
#' 
#' @param wday Day of week (integer 1-7)
#' @param from Date. Can be a vector.
#' @param to Date. Same length as `from` and must be greater than `from`.
#' @param week_start 1 = Monday. 7 = Sunday
#' 
wday_in_interval = function(wday, from, to, week_start = 1) {
  if (wday < 1 | weekday > 7) 
    stop("wday must be an integer from 1 to 7.")
  if (week_start)
    wday = 1 + (((wday - 2) + week_start ) %% 7)  # Translate wday to week_start = 1 (ISO standard)
  if (any(from > to, na.rm = TRUE))
    stop("`from` must come before `to`")
  
  # If the interval is greater than a week, it trivially contains any weekday
  over_a_week = difftime(from, to, units = "days") >= 7
  
  # Check if weekday is both smaller/greater than "from" and "to"
  days_from = as.numeric(strftime(from, "%u"))
  days_to = as.numeric(strftime(to, "%u"))
  contains_weekday = ifelse(
    strftime(from, "%V") == strftime(to, "%V"),  # Dates are in the same week?
    yes = wday >= days_from & wday <= days_to,
    no = wday >= days_from | wday <= days_to  # 
  )
  
  return(over_a_week | contains_weekday)
}

As an example, say we want to detect intervals in a time series that overlaps weekends. We run wday_in_interval for Saturday and Sunday:

library(dplyr)
tibble::tibble(
  timestamp = seq(as.POSIXct("2020-09-03 0:00"), as.POSIXct("2020-09-8 12: 00"), length.out = 10),
  overlaps_saturday = wday_in_interval(6, from = lag(timestamp), to = timestamp),
  overlaps_sunday = wday_in_interval(7, from = lag(timestamp), to = timestamp),
  overlaps_weekend = overlaps_saturday | overlaps_sunday
)

Result:

# A tibble: 10 x 4
   timestamp           overlaps_saturday overlaps_sunday overlaps_weekend
   <dttm>              <lgl>             <lgl>           <lgl>           
 1 2020-09-03 00:00:00 NA                NA              NA              
 2 2020-09-03 14:40:00 FALSE             FALSE           FALSE           
 3 2020-09-04 05:20:00 FALSE             FALSE           FALSE           
 4 2020-09-04 20:00:00 FALSE             FALSE           FALSE           
 5 2020-09-05 10:40:00 TRUE              FALSE           TRUE            
 6 2020-09-06 01:20:00 TRUE              TRUE            TRUE            
 7 2020-09-06 16:00:00 FALSE             TRUE            TRUE            
 8 2020-09-07 06:40:00 FALSE             TRUE            TRUE            
 9 2020-09-07 21:20:00 FALSE             FALSE           FALSE           
10 2020-09-08 12:00:00 FALSE             FALSE           FALSE  

On my mid-range laptop, it processes 250.000 rows in ~3 seconds.

Upvotes: 0

CL.
CL.

Reputation: 14957

There already are good solutions, but none of them avoids generating a sequence of days. I tried to find a solution that just compares weekday numbers (and weeks). It internally uses Monday as first day of the week, but the argument startWithSunday provides the possibility to set Sunday as day 1. An alternative would have been to switch between %V and %U in strftime, but this approach seems more straightforward to me.

isDayIn1 <- function(weekday, date1, date2, startWithSunday = FALSE) {

  if (weekday < 1 | weekday > 7) stop("weekday must be an integer from 1 to 7.")

  if(startWithSunday) {
    weekday <- max(weekday - 1, 1)
  }

  dates <- sort(as.Date(c(date1, date2)))

  if (dates[2] - dates[1] >= 7) return(TRUE)

  weeks <- strftime(dates, "%V")
  days  <- strftime(dates, "%u")

  if (weeks[1] == weeks[2]) { # Dates are in the same week.
    return(weekday >= days[1] & weekday <= days[2])
  } else { # Different weeks.
    return(weekday >= days[1] | weekday <= days[2])
  }
}

The function looks like a lot of code for such a small task, but most of it is just preparation; the actual work is done in the two return statements. The trick is to distinguish cases where the dates are within the same vs. in different weeks because this affects the comparison we ought to do.

To check if isDayIn1 does it's job, I wrote this small wrapper function:

niceTests <- function(weekday, date1, date2, startWithSunday = FALSE) {

  date1 <- as.Date(date1)
  date2 <- as.Date(date2)

  fmt <- "%a, %y-%m-%d (week %V)"
  if (startWithSunday) {
    fmt <- "%a, %y-%m-%d (week %U)"
  }
  print(sprintf("Date1: %s, Date2: %s, Diff.: %d. Range contains day #%d: %s",
                strftime(date1, fmt),
                strftime(date2, fmt),
                abs(date2 - date1),
                weekday,
                as.character(isDayIn1(weekday, date1, date2, startWithSunday))
                ))
}

And here's the first bunch of tests. Note that startWithSunday defaults to FALSE, so here weekday 1 means Monday.

niceTests(7, "2015-08-02", "2015-08-03") # from question (Sunday in Su-Mo)
niceTests(6, "2015-08-02", "2015-08-03") # from question (Saturday in Su-Mo)
niceTests(1, "2015-08-02", "2015-08-09") # Full week or more.
niceTests(1, "2015-08-02", "2015-08-10") # Full week or more.

niceTests(1, "2015-08-05", "2015-08-07") # Same week. (Wednesday - Friday)
niceTests(2, "2015-08-05", "2015-08-07") # Same week.
niceTests(3, "2015-08-05", "2015-08-07") # Same week.
niceTests(4, "2015-08-05", "2015-08-07") # Same week.
niceTests(5, "2015-08-05", "2015-08-07") # Same week.
niceTests(6, "2015-08-05", "2015-08-07") # Same week.
niceTests(7, "2015-08-05", "2015-08-07") # Same week.

niceTests(1, "2015-08-08", "2015-08-11") # Across weeks. (Saturday - Tuesday)
niceTests(2, "2015-08-08", "2015-08-11") # Across weeks.
niceTests(3, "2015-08-08", "2015-08-11") # Across weeks.
niceTests(4, "2015-08-08", "2015-08-11") # Across weeks.
niceTests(5, "2015-08-08", "2015-08-11") # Across weeks.
niceTests(6, "2015-08-08", "2015-08-11") # Across weeks.
niceTests(7, "2015-08-08", "2015-08-11") # Across weeks.

Output:

[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-03 (week 32), Diff.: 1. Range contains day #7: TRUE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-03 (week 32), Diff.: 1. Range contains day #6: FALSE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Sun, 15-08-09 (week 32), Diff.: 7. Range contains day #1: TRUE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-10 (week 33), Diff.: 8. Range contains day #1: TRUE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #1: FALSE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #2: FALSE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #3: TRUE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #4: TRUE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #5: TRUE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #6: FALSE"
[1] "Date1: Wed, 15-08-05 (week 32), Date2: Fri, 15-08-07 (week 32), Diff.: 2. Range contains day #7: FALSE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #1: TRUE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #2: TRUE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #3: FALSE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #4: FALSE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #5: FALSE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #6: TRUE"
[1] "Date1: Sat, 15-08-08 (week 32), Date2: Tue, 15-08-11 (week 33), Diff.: 3. Range contains day #7: TRUE"

Finally, tests for startWidthSunday = TRUE where day 1 is Sunday:

print("Now: Start with Sunday!")

niceTests(1, "2015-08-02", "2015-08-03", startWithSunday = TRUE) # from question (Sunday in Su-Mo)
niceTests(7, "2015-08-02", "2015-08-03", startWithSunday = TRUE) # from question (Saturday in Su-Mo)
niceTests(1, "2015-08-02", "2015-08-09", startWithSunday = TRUE) # Full week or more.
niceTests(1, "2015-08-02", "2015-08-10", startWithSunday = TRUE) # Full week or more.

niceTests(1, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week. (Wednesday - Friday)
niceTests(2, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.
niceTests(3, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.
niceTests(4, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.
niceTests(5, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.
niceTests(6, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.
niceTests(7, "2015-08-05", "2015-08-07", startWithSunday = TRUE) # Same week.

niceTests(1, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks. (Saturday - Tuesday)
niceTests(2, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.
niceTests(3, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.
niceTests(4, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.
niceTests(5, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.
niceTests(6, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.
niceTests(7, "2015-08-08", "2015-08-11", startWithSunday = TRUE) # Across weeks.

Output:

[1] "Now: Start with Sunday!"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-03 (week 31), Diff.: 1. Range contains day #1: TRUE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-03 (week 31), Diff.: 1. Range contains day #7: FALSE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Sun, 15-08-09 (week 32), Diff.: 7. Range contains day #1: TRUE"
[1] "Date1: Sun, 15-08-02 (week 31), Date2: Mon, 15-08-10 (week 32), Diff.: 8. Range contains day #1: TRUE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #1: FALSE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #2: FALSE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #3: FALSE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #4: TRUE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #5: TRUE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #6: TRUE"
[1] "Date1: Wed, 15-08-05 (week 31), Date2: Fri, 15-08-07 (week 31), Diff.: 2. Range contains day #7: FALSE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #1: TRUE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #2: TRUE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #3: TRUE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #4: FALSE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #5: FALSE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #6: FALSE"
[1] "Date1: Sat, 15-08-08 (week 31), Date2: Tue, 15-08-11 (week 32), Diff.: 3. Range contains day #7: TRUE"

Upvotes: 1

alexwhitworth
alexwhitworth

Reputation: 4907

I think your solution is fine. But here's a quick fix:

isDayIn <- function(weekday, date1, date2) {
  if (weekday<1 | weekday>7) stop("weekday must be an integer from 1 to 7.")
  require(lubridate)
  date1 <- as.Date(date1)
  date2 <- as.Date(date2)
  if (as.integer(date2 - date1) >= 7) {
    return(TRUE) # by default
  } else {
    return(weekday %in% wday(seq.Date(date1, date2, by=1)))
  }
}

Upvotes: 1

Pierre L
Pierre L

Reputation: 28441

Another function option using base R:

isDayIn <- function(weekday, date1, date2) {
  if (weekday<1 | weekday>7) stop("weekday must be an integer from 1 to 7.")
  weekday %in% strftime(seq(as.Date(date1), as.Date(date2), by="day"), format="%w")
}

isDayIn(1, "2015-08-02", "2015-08-03")
[1] TRUE
isDayIn(7, "2015-08-02", "2015-08-03")
[1] FALSE

Upvotes: 2

Related Questions