|
1 | | -#' Get the fiscal term of a date-time |
| 1 | +#' Get create a term data frame |
2 | 2 | #' |
3 | | -#' Quarters divide the year into fourths. Trimesters divide the year into thirds. Semesters divide the year into halfs. |
4 | | -#' |
5 | | -#' @param x a date-time object of class POSIXct, POSIXlt, Date, chron, yearmon, yearqtr, |
6 | | -#' zoo, zooreg, timeDate, xts, its, ti, jul, timeSeries, fts or anything else that can |
7 | | -#' be converted with as.POSIXlt |
8 | | -#' @param term the way you are dividing the year, be it semesters, trimesters, or quarters. |
9 | | -#' @param type the format to be returned for the quarter. Can be one one of "quarter" - |
10 | | -#' return numeric quarter (default), "year.quarter" return the ending year and quarter |
11 | | -#' as a number of the form year.quarter, "date_first" or "date_last" - return the date |
12 | | -#' at the quarter's start or end, "year_start/end" - return a full description of the |
13 | | -#' quarter as a string which includes the start and end of the year |
14 | | -#' (ex. "2020/21 Q1"). |
15 | | -#' @param fiscal_start numeric indicating the starting month of a fiscal year. |
| 3 | +#' @param start_date The first day of the term in year month day format. |
| 4 | +#' @param weeks The length of the term in weeks. Must be a singular numeric value, |
| 5 | +#' automates to 10 |
| 6 | +#' @param skip list of dates for which there may not be school, and pushes the end of the term back |
| 7 | +#' @param holidays list of dates indicating major holidays, marks it in the final |
| 8 | +#' data frame as a holiday, but does not add length to the term. |
| 9 | +#' @param class_days To indicate which days of the week there are class. |
| 10 | +#' For example, if Tuesday and Friday are class days the input must be written |
| 11 | +#' like c(2, 5), c('Tue', 'Fri'), c('Tu', 'F'), c('Tuesday', 'Friday'), |
| 12 | +#' or c('Tue', 'Friday'), etc |
16 | 13 | #' @param holidays list of dates indicating major holidays |
17 | | -#' @param split_count should you want to split up the year in your own number you can |
18 | | -#' use an "other" and theninput your own division count as a numeric to split_count |
| 14 | +#' @param exams list of exam dates |
| 15 | + |
| 16 | + |
| 17 | +set_term <- function(start_date= "2025-01-01", |
| 18 | + weeks = 10, |
| 19 | + skip= NULL, |
| 20 | + holidays= NULL, |
| 21 | + class_days = NULL, |
| 22 | + exams = NULL){ |
| 23 | + |
| 24 | + class_days <- clean_class_days(class_days) |
| 25 | + total_skip <- 0 |
19 | 26 |
|
20 | | -term <- function(x, term = "quarter", type = "term", fiscal_start = 1, holidays = NULL, split_count = NULL){ |
21 | | - if (length(fiscal_start) > 1) { |
22 | | - stop("`fiscal_start` must be a singleton", call. = FALSE) |
| 27 | + if (is.null(weeks)||!is.numeric(weeks)) { |
| 28 | + stop("weeks parameter must contain a numeric value") |
23 | 29 | } |
24 | | - fs <- (fiscal_start - 1) %% 12 |
25 | | - shifted <- seq(fs, 11 + fs) %% 12 + 1 |
26 | | - m <- month(x) |
27 | | - s <- match(m, shifted) |
28 | | - |
29 | | - num_divisions <- switch(term, |
30 | | - "quarter" = 4, |
31 | | - "trimester" = 3, |
32 | | - "semester" = 2, |
33 | | - "other" = split_count, |
34 | | - stop("Unsupported type: ", term)) |
35 | | - |
36 | | - divisions <- rep(1:num_divisions, each = 12 / num_divisions) |
37 | | - q <- divisions[s] |
38 | | - |
39 | | - if (is.logical(type)) { |
40 | | - type <- if (type) "year.term" else "term" |
| 30 | + |
| 31 | + if (is.null(start_date)) { |
| 32 | + stop("start_date parameter must contain a date in year-month-day form") |
41 | 33 | } |
42 | | - |
43 | | - |
44 | | - if (!is.null(holidays)) { |
45 | | - is_holiday <- as.Date(x) %in% as.Date(holidays) |
46 | | - return(list(division = current_division, holiday = is_holiday)) |
| 34 | + all_skip_dates <- c() |
| 35 | + if (!is.null(skip)) { |
| 36 | + if (!all(c("start", "end") %in% names(skip))) { |
| 37 | + stop("`skip` data frame must contain `start` and `end` columns.") |
| 38 | + } |
| 39 | + |
| 40 | + skip$end <- ymd(skip$'end') |
| 41 | + skip$start <- ymd(skip$'start') |
| 42 | + for(i in 1:nrow(skip)){ |
| 43 | + skip_range <- seq(ymd(skip$start[i]), |
| 44 | + ymd(skip$end[i]), |
| 45 | + by='days') |
| 46 | + |
| 47 | + all_skip_dates <- unique(c(all_skip_dates, |
| 48 | + skip_range)) |
| 49 | + } |
| 50 | + total_skip <- length(all_skip_dates) |
| 51 | + |
| 52 | + |
47 | 53 | } |
48 | 54 |
|
49 | | - switch(type, |
50 | | - "term" = q, |
51 | | - "year_start/end" = , |
52 | | - "year.term" = { |
53 | | - nxt_year_months <- if (fs != 0) (fs + 1):12 |
54 | | - y = year(x) + (m %in% nxt_year_months) |
55 | | - out = y + (q / 10) |
56 | | - if (type == "year_start/end") { |
57 | | - out = sprintf("%d/%d Q%d", y - 1, y %% 100, q) |
58 | | - } |
59 | | - out |
60 | | - }, |
61 | | - "date_first" = , |
62 | | - "date_last" = { |
63 | | - starting_months <- shifted[seq(1, length(shifted), 3)] |
64 | | - final_years <- year(x) - (starting_months[q] > m) |
65 | | - quarter_starting_dates <- |
66 | | - make_date(year = final_years, month = starting_months[q], day = 1L) |
67 | | - if (type == "date_first") { |
68 | | - quarter_starting_dates |
69 | | - } else if (type == "date_last") { |
70 | | - add_with_rollback(quarter_starting_dates, months(3)) - days(1) |
71 | | - } |
72 | | - }, |
73 | | - stop("Unsupported type ", type) |
| 55 | + start_date <- ymd(start_date) |
| 56 | + total_days <- weeks*7 + total_skip |
| 57 | + end_date <- start_date +days(total_days) |
| 58 | + date_range <- seq(start_date, end_date, by='days') |
| 59 | + |
| 60 | + term <- data.frame( |
| 61 | + date = date_range, |
| 62 | + day = weekdays(date_range), |
| 63 | + status = "no class", |
| 64 | + stringsAsFactors = FALSE, |
| 65 | + class = FALSE |
74 | 66 | ) |
| 67 | + |
| 68 | + term$status[term$day %in% class_days] <- "class" |
| 69 | + term$status[term$date %in% all_skip_dates] <-"skip" |
| 70 | + term$status[term$date %in% ymd(holidays)] <- "holiday" |
| 71 | + term$status[term$date %in% ymd(exams)] <- "exam" |
| 72 | + term$class[term$status %in% c("class", "exam")] <- TRUE |
| 73 | + return(term) |
75 | 74 | } |
| 75 | + |
| 76 | + |
| 77 | +clean_class_days <- function(class_days) { |
| 78 | + week_days <- c() |
| 79 | + valid_days <- c("Monday", "Tuesday", "Wednesday", |
| 80 | + "Thursday", "Friday", "Saturday", "Sunday") |
| 81 | + short_days <- c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") |
| 82 | + letter_days <- c("M", "Tu", "W", "Tr", "F", "Sa", "Su") |
| 83 | + |
| 84 | + |
| 85 | + if (is.null(class_days)) { |
| 86 | + return(c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")) |
| 87 | + } |
| 88 | + |
| 89 | + for (i in seq_along(class_days)) { |
| 90 | + day <- class_days[i] |
| 91 | + |
| 92 | + #numeric input |
| 93 | + if (is.numeric(day)) { |
| 94 | + if (day >= 1 && day <= 7) { |
| 95 | + week_days <- c(week_days, valid_days[day]) |
| 96 | + } else { |
| 97 | + stop("Numeric `class_days` values must be between 1 and 7.") |
| 98 | + } |
| 99 | + } |
| 100 | + #short names like "Mon", "Tue" |
| 101 | + else if (day %in% short_days) { |
| 102 | + week_days <- c(week_days, valid_days[match(day, short_days)]) |
| 103 | + } |
| 104 | + #single-letter abbreviations like "M", "Tu" |
| 105 | + else if (day %in% letter_days) { |
| 106 | + week_days <- c(week_days, valid_days[match(day, letter_days)]) |
| 107 | + } |
| 108 | + #full names like "Monday", "Tuesday" |
| 109 | + else if (day %in% valid_days) { |
| 110 | + week_days <- c(week_days, day) |
| 111 | + } |
| 112 | + #invalid input |
| 113 | + else { |
| 114 | + stop("Invalid `class_days` value. Must be a valid day of the week. |
| 115 | + For example, if Tuesday, must be written like c(2), |
| 116 | + c('Tue'), c('Tu'), or c('Tuesday')") |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + return(unique(week_days)) |
| 121 | +} |
| 122 | + |
| 123 | +#' @rdname set_term |
| 124 | +#' @export |
0 commit comments