R/cron.R

Defines functions cron2eng r2cron

Documented in cron2eng r2cron

#' Build Cron Strings from R Expressions
#'
#' Provide an R expression to each of the arguments, which are different time
#' periods (minutes, hours, days of the month, months, days of the week), and
#' receive an equivalent cron-expression string in return. See 'arguments' for
#' valid value ranges and 'details' for valid input formats.
#'
#' @param minutes Minutes past the hour, integers 0 to 59 inclusive.
#' @param hours Hours on a 24-hour clock, integers 0 to 23 inclusive.
#' @param days_month Day number of the month, integers 1 to 31 inclusive.
#' @param months Month number, integers 1 to 12 inclusive.
#' @param days_week Day of the week, integers 1 to 7 inclusive (where Sunday is
#'     the first day of the week).
#' @param clip Logical. Copy output to clipboard? Windows, macOS and X11 only.
#'     Requires installation of the {clipr} package.
#'
#' @details The time-period arguments default to every unit within that time
#'     period, like 'every minute, of every hour, of every day of the month,
#'     etc'. The following input formats are acceptable inputs to all the time
#'     period arguments:
#' \itemize{
#'   \item a single integer value, like \code{1}
#'   \item consecutive-integer vectors, like \code{1:3}
#'   \item nonconsecutive, irregularly-spaced integer vectors, like
#'       \code{c(2, 3, 5)}
#'   \item regularly-spaced integer sequences with specified start and end
#'   values, like \code{seq(3, 59, 15)} (useful for specifying sequences within
#'   the full time period,'every 15 minutes of the hour starting from minute 3',
#'   like in this example)
#' }
#'
#' @return A character string (copied to the clipboard at the user's
#'     discretion).
#' @export
#'
#' @examples \dontrun{
#' r2cron(
#'   minutes = seq(0, 59, 20),
#'   hours = 15:17,  # 24-hr clock
#'   days_month = 1,
#'   months = c(4, 10, 11),
#'   days_week = c(1, 7),  # Sunday is '1'
#'   clip = FALSE
#' )}
#'
r2cron <- function(minutes = 0L:59L, hours  = 0L:23L,
                   days_month  = 1L:31L, months = 1L:12L, days_week  = 1L:7L,
                   clip = FALSE) {

 # Stop if input is out of range of time period
 .stop(minutes, "minutes")
 .stop(hours, "hours")
 .stop(days_month, "days_month")
 .stop(months, "months")
 .stop(days_week, "days_week")

 # Zero-index the days of the week
 days_week <- days_week - 1

 # Convert each argument's R expression to equivalent cron expression
 cron_minutes <- .as_cron(minutes, "minutes")
 cron_hours <- .as_cron(hours, "hours")
 cron_days_month <- .as_cron(days_month, "days_month")
 cron_months <- .as_cron(months, "months")
 cron_days_week <- .as_cron(days_week, "days_week")

 # Warn if month and day combinations seem unlikely
 if (cron_months != "*" & cron_days_month != "*" &
   any(months %in% c(2, 4, 6, 9, 11)) & any(days_month == 31)) {
   warning("\n  Sure? There's no 31st in Feb, Apr, Jun, Sept nor Nov.\n")
 }
 if (cron_months != "*" & cron_days_month != "*" &
     any(months == 2) & any(days_month == 30)) {
   warning("\n  Sure? There's no 30th in Feb.\n")
 }
 if (cron_months != "*" & cron_days_month != "*" &
     any(months == 2) & any(days_month == 29)) {
   warning("\n  Sure? 29 Feb is only in leap years.\n")
 }

 # Paste together cron expressions for each time period
 cron_string <- paste(
  cron_minutes, cron_hours, cron_days_month, cron_months, cron_days_week
 )

 # Copy the string to the user's clipboard if requested
 if (clip) {
  clipr::write_clip(cron_string, allow_non_interactive = TRUE)
  cat("Copied to clipboard\n")
 }

 # Return the full cron expression
 return(cron_string)

}

#' Generate English Interpretation of Valid Cron Strings
#'
#' Demystify the meaning of a valid cron string by converting it to an
#' equivalent sentence in English. Can take the output from \code{\link{r2cron}}
#' for example. (Under development.)
#'
#' @param cron Character. A valid cron expression, i.e. a string with five
#'     time period 'slots' (minutes, hours, days of the month, months, days of
#'     the week), separated by spaces. See details for more information.
#'
#' @details The cron string slots and their valid ranges are:
#' \itemize{
#'   \item Slot 1: minutes past the hour, integers 0 to 59 inclusive.
#'   \item Slot 2: hours on a 24-hour clock, integers 0 to 23 inclusive.
#'   \item Slot 3: day number of the month, integers 1 to 31 inclusive.
#'   \item Slot 4: month number, integers 1 to 12 inclusive.
#'   \item Slot 5: day of the week, integers 0 to 6 inclusive (where Sunday is
#'     the first day of the week).
#' }
#'
#' In addition, the following input formats are acceptable to all the time
#' slots:
#' \itemize{
#'   \item a single integer value, like \code{1}
#'   \item consecutive-integer vectors, like \code{1:3}
#'   \item nonconsecutive, irregularly-spaced integer vectors, like
#'       \code{c(2, 3, 5)}
#'   \item regularly-spaced integer sequences with specified start and end
#'       values, like \code{seq(3, 59, 15)} (useful for specifying
#'       sequences within the full time period,'every 15 minutes of the
#'       hour starting from minute 3', like in this example)
#' }
#'
#' @return Result printed to console. An English sentence interpretation of the
#'     cron string that was the input.
#' @export
#'
#' @examples \dontrun{
#' cron2eng("1,2,5 2-3 * 1/3 5")}
#'
cron2eng <- function(cron = "* * * * *") {

  if (!is.character(cron)) {
    stop("Argument 'cron' must be a valid character-class cron expression.")
  }

  if (length(strsplit(cron, " ")[[1]]) != 5) {
    stop("Argument 'cron' must be a valid cron expression.")
  }

  if (length(strsplit(cron, " ")[[1]]) != 5 |  # 5 time slots
      !all(grepl("\\d|\\*|\\s|,|/|-", strsplit(cron, "")[[1]])) |  # valid chars
      !all(grepl("\\b[0-9]\\b|\\b[1-5][0-9]\\b|\\*",
                 strsplit(cron, "\\s|,|/|-")[[1]])) ) {  # valid number range
    stop(
      "Argument 'cron' must have five 'time slots' separated by spaces.\n",
      "Slots must contain valid integers or '*', '/', '-' and ',."
    )
  }

  # Split each time period to a list element
  p_list <- as.list(strsplit(cron, " ")[[1]])

  # Name elements with text to be used in output for that time period
  names(p_list) <- c(
    "minute(s)", "hour(s)", "day(s) of the month",
    "month(s)", "day(s) of the week"
  )

  if (grepl("\\d", p_list[[2]])) {  # if minutes slot contains a number

    if (grepl("/", p_list[[2]])) {  # if minutes slot contains a slash

      # Extract value "x" (start hour) from "x/y"
      slot_val <- as.numeric(strsplit(p_list[[2]], "/")[[1]][1])

      # Replace "x" with corresponding time string (hour)
      if (slot_val == 0) {
        replacement <- "12AM"
      } else if (slot_val >= 1 & slot_val <= 11) {
        replacement <- paste0(slot_val, "AM")
      } else if (slot_val == 12) {
        replacement <- "12PM"
      } else if (slot_val >= 13 & slot_val <= 23) {
        replacement <- paste0(slot_val - 12, "PM")
      }

      # Replace the "x" in "x/y" with time string (hour)
      p_list[[2]] <-
        gsub("^\\d{1,2}(?=/)", replacement, p_list[[2]], perl = TRUE)

    } else {

      # Find/replace any hours in the form "x", "x,y,z", "x-y"
      p_list[[2]] <- gsub("\\b0\\b",  "12AM", p_list[[2]])
      p_list[[2]] <- gsub("\\b1\\b",  "1AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b2\\b",  "2AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b3\\b",  "3AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b4\\b",  "4AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b5\\b",  "5AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b6\\b",  "6AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b7\\b",  "7AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b8\\b",  "8AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b9\\b",  "9AM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b10\\b", "10AM", p_list[[2]])
      p_list[[2]] <- gsub("\\b11\\b", "11AM", p_list[[2]])
      p_list[[2]] <- gsub("\\b12\\b", "12PM", p_list[[2]])
      p_list[[2]] <- gsub("\\b13\\b", "1PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b14\\b", "2PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b15\\b", "3PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b16\\b", "4PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b17\\b", "5PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b18\\b", "6PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b19\\b", "7PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b20\\b", "8PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b21\\b", "9PM",  p_list[[2]])
      p_list[[2]] <- gsub("\\b22\\b", "10PM", p_list[[2]])
      p_list[[2]] <- gsub("\\b23\\b", "11PM", p_list[[2]])

    }

  }

  if (grepl("\\d", p_list[[4]])) {  # if months slot contains a number

    if (grepl("/", p_list[[4]])) {  # if months slot contains a slash

      # Extract value "x" (start month) from "x/y"
      slot_val <- as.numeric(strsplit(p_list[[4]], "/")[[1]][1])

      # Replace "x" with corresponding time string (month)
      replacement <- month.name[slot_val]

      # Replace the "x" in "x/y" with time string (month)
      p_list[[4]] <-
        gsub("^\\d{1,2}(?=/)", replacement, p_list[[4]], perl = TRUE)

    } else {

      # Find/replace any months in the form "x", "x,y,z", "x-y"
      p_list[[4]] <- gsub("\\b1\\b",  "January",   p_list[[4]])
      p_list[[4]] <- gsub("\\b2\\b",  "February",  p_list[[4]])
      p_list[[4]] <- gsub("\\b3\\b",  "March",     p_list[[4]])
      p_list[[4]] <- gsub("\\b4\\b",  "April",     p_list[[4]])
      p_list[[4]] <- gsub("\\b5\\b",  "May",       p_list[[4]])
      p_list[[4]] <- gsub("\\b6\\b",  "June",      p_list[[4]])
      p_list[[4]] <- gsub("\\b7\\b",  "July",      p_list[[4]])
      p_list[[4]] <- gsub("\\b8\\b",  "August",    p_list[[4]])
      p_list[[4]] <- gsub("\\b9\\b",  "September", p_list[[4]])
      p_list[[4]] <- gsub("\\b10\\b", "October",   p_list[[4]])
      p_list[[4]] <- gsub("\\b11\\b", "November",  p_list[[4]])
      p_list[[4]] <- gsub("\\b12\\b", "December",  p_list[[4]])

    }

  }

  if (grepl("\\d", p_list[[5]])) {

    if (grepl("/", p_list[[5]])) {

      # Extract value "x" (start hour) from "x/y"
      slot_val <- as.numeric(strsplit(p_list[[5]], "/")[[1]][1]) + 1

      # Replace "x" with corresponding time string (day of week)
      replacement <- c("Sunday", "Monday", "Tuesday", "Wednesday",
                       "Thursday", "Friday", "Saturday")[slot_val]

      # Replace the "x" in "x/y" with time string (day of week)
      p_list[[5]] <-
        gsub("\\d{1,2}(?=/)", replacement, p_list[[5]], perl = TRUE)

    } else {

      # Find/replace any days-of-week in the form "x", "x,y,z", "x-y"
      p_list[[5]] <- gsub("0", "Sunday",    p_list[[5]])
      p_list[[5]] <- gsub("1", "Monday",    p_list[[5]])
      p_list[[5]] <- gsub("2", "Tuesday",   p_list[[5]])
      p_list[[5]] <- gsub("3", "Wednesday", p_list[[5]])
      p_list[[5]] <- gsub("4", "Thursday",  p_list[[5]])
      p_list[[5]] <- gsub("5", "Friday",    p_list[[5]])
      p_list[[5]] <- gsub("6", "Saturday",  p_list[[5]])

    }

  }

  # Convert each time slot's cron expression to English
  for (p in names(p_list)) {

    if (p_list[[p]] == "*") {

      # Every valid unit of the time slot
      if (p == names(p_list)[5]) {
        p_list[[p]] <- paste("any", p)  # more specific for day of week
      } else {
        p_list[[p]] <- paste("every", p)
      }

    } else if (grepl("^\\d{1,2}$|^\\w{1,}$", p_list[[p]])) {

      # Single integer value
      p_list[[p]] <- paste(p, p_list[[p]])

    } else if (grepl("^\\w{1,}-\\w{1,}$", p_list[[p]])) {

      # Consecutive units with non-min start and non-max stop value
      n_split <- strsplit(p_list[[p]], "-")
      p_list[[p]] <- paste(
        p, n_split[[1]][1], "to", n_split[[1]][2]
      )

    } else if (grepl("^\\w{1,},", p_list[[p]])) {

      # Nonconsecutive, irregularly spaced integers
      n_units <- strsplit(p_list[[p]], ",")[[1]]
      p_list[[p]] <- paste(p, .vec2eng(n_units))

    } else if (grepl("^\\w{1,}/\\w{1,}$", p_list[[p]])) {

      # regularly-spaced integer sequences with specified start and end
      n_split <- strsplit(p_list[[p]], "/")
      p_list[[p]] <- paste0(
        "every ", n_split[[1]][2], " ", p,
        " starting from ",
        ifelse(p %in% c("minute(s)", "day(s)"), paste0(p, " "), ""),
        n_split[[1]][1]
      )

    }

  }

  # Collate sentences for each time period
  cat(
    paste0("Cron string '", cron, "' means:"),
    paste0("  - ", p_list[[1]]),
    paste0("  - ", p_list[[2]]),
    paste0("  - ", p_list[[3]]),
    paste0("  - ", p_list[[4]]),
    ifelse(  # a clear 'and' for user-specified days of the week
      grepl("^day", p_list[[5]]),
      paste0("  - and ", p_list[[5]]),
      paste0("  - ", p_list[[5]])
    ),
    sep = "\n"
  )

}
matt-dray/dialga documentation built on Dec. 21, 2021, 2:54 p.m.