Reputation: 5157
[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
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
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
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
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