R/input-check.R

Defines functions check_vector check_ks check_df_variables check_across_subset_negate check_one_of check_none_of check_h_k check_directories check_data_frame check_no_overlap check_assign are_vectors are_values is_vec is_value is_any_of is_list is_color extract_feedback give_feedback lazy_check_dummy

Documented in are_values are_vectors check_across_subset_negate check_assign check_data_frame check_df_variables check_directories check_h_k check_ks check_none_of check_no_overlap check_one_of check_vector extract_feedback give_feedback is_any_of is_color is_list is_value is_vec lazy_check_dummy

#' @title This is a text dummy.
#'
#' @description A member of the \code{lazy-check_*()}-family.
#'
#' @details Members of the \code{lazy-check_*()}-family take the arguments
#' of their kind - that are used in the function they are called in - and
#' checks whether these arguments input fit the requirements. They stop and return an
#' error immediately once they stumble upon something invalid. They do not alter or adjust input
#' and return TRUE if the whole function has been executed without anything
#' invalid being found.
#'
#' @return A logical value TRUE if nothing invalid has been detected or an informative
#' feedback message.

lazy_check_dummy <- function(){}


#' @title Print feedback in console
#'
#' @description Helper that gives feedback with a function of choice.
#'
#' @param fdb.fn Character value. Determines the function to call if a feedback
#' message needs to be given. One of \emph{'stop', 'warning'} or \emph{'message'}.
#' @param msg Character value or glue. The message to be printed in the console.
#' @param in.shiny Allows to use the function to stop a function without crashing
#' a shiny session.
#' @param with.time Logical value. Indicates whether the current time is to be
#' added to the feedback message.
#' @inherit verbose params
#'
#' @return
#' @export
#'

give_feedback <- function(fdb.fn = "message", msg = NULL, in.shiny = FALSE, with.time = TRUE, verbose = TRUE, ...){

  if(!base::is.null(msg) && base::isTRUE(with.time)){

    time <- base::Sys.time()

    hours <- lubridate::hour(time)

    ref_hours <-
      base::ifelse(
        test = stringr::str_length(hours) == 1,
        yes = stringr::str_c(0, hours, sep = ""),
        no = hours)

    minutes <- lubridate::minute(time)

    ref_minutes <-
      base::ifelse(
        test = stringr::str_length(minutes) == 1,
        yes = stringr::str_c(0, minutes, sep = ""),
        no = minutes)

    seconds <- base::round(lubridate::second(time), digits = 0)

    ref_seconds <-
      base::ifelse(
        test = stringr::str_length(seconds) == 1,
        yes = stringr::str_c(0, seconds, sep = ""),
        no = seconds)

    time_string <-
      stringr::str_c(
        ref_hours,
        ref_minutes,
        ref_seconds,
        sep = ":"
      )

    msg <- glue::glue("{time_string} {msg}")

  }

  if((base::isTRUE(in.shiny) | shiny::isRunning()) && !base::is.null(msg)){

    if(fdb.fn == "stop"){

      shiny_fdb(in.shiny = TRUE, ui = msg, type = "error", ...)

      shiny::req(FALSE)

    } else if(fdb.fn == "warning") {

      shiny_fdb(in.shiny = TRUE, ui = msg, type = "warning", ...)

    } else if(fdb.fn == "message" & base::isTRUE(verbose)){

      shiny_fdb(in.shiny = TRUE, ui = msg, type = "message", ...)

    }

  } else if(!base::is.null(msg)){

    if(fdb.fn == "stop"){

      base::stop(msg)

    } else if(fdb.fn == "warning"){

      base::warning(msg)

    } else if(fdb.fn == "message" && base::isTRUE(verbose)){

      base::message(msg)

    }

  }

}



#' Title
#'
#' @description Returns the appropriate string to extract the feedback
#' from \code{purr::quietly()} results.
#'
#' @inherit give_feedback params
#'

extract_feedback <- function(fdb.fn){

  if(fdb.fn == "message"){

    return("messages")

  } else if(fdb.fn == "warning"){

    return("warnings")

  } else if(fdb.fn == "stop"){

    return("stop")

  }

}


# is - functions ----------------------------------------------------------

#' Check if elements in a character vector represent valid colors.
#'
#' This function checks if each element in a character vector represents a valid
#' color. It performs two checks:
#' - It tests whether each element in the vector matches the pattern of a valid
#'   hexadecimal color code (e.g., "#RRGGBB").
#' - It tests whether each element in the vector is one of the recognized color
#'   names in R.
#'
#' @param vector A character vector containing color names or hexadecimal color
#'               codes.
#'
#' @return A logical vector of the same length as 'vector', where each element
#'         is 'TRUE' if the corresponding element in 'vector' represents a valid
#'         color, and 'FALSE' otherwise.
#'
#' @examples
#' is_color(c("#FF0000", "blue", "invalid", "green"))
#'
#' @export

is_color <- function(vector) {

  purrr::map_lgl(
    .x = vector,
    .f = function(color){

      tryCatch({

        # Attempt to convert the color to RGB
        col2rgb(color)

        # If successful, return TRUE
        TRUE

      }, warning = function(w) {

        # If a warning is thrown, return FALSE
        FALSE

      }, error = function(e){

        # IF an error is thrown, return FALSE
        FALSE

      })

    }
  )
}



#' @title List input check
#'
#' @param input Object to be checked.
#'
#' @return Boolean
#' @export
#'

is_list <- function(input){

  base::all(base::is.list(input) && !base::is.data.frame(input))

}



#' @title Any input check
#'
#' @param input Object to be checked.
#' @inherit argument_dummy params
#'
#' @return TRUE if \code{input} is of at least one of
#' the specified classes in \code{valid.classes}. FALSE
#' if not.
#'
#' @export

is_any_of <- function(input, valid.classes){

  res_lgl <-
    purrr::map_lgl(
      .x = valid.classes,
      .f = ~ base::is.vector(x = input, mode = .x)
      )

  if("factor" %in% valid.classes | "any" %in% valid.classes){

    res_lgl <- c(res_lgl, base::is.factor(input))

  }

  base::any(res_lgl)

}



#' @title One dimensional input check
#'
#' @description Checks if input fits the requirements and gives feedback
#' via \code{give_feedback()}.
#'
#' @param x Input vector.
#' @param ... Character vector denoting the objects to be checked.
#' @param return Character value. Either \emph{'boolean'} which makes the function return an
#' invisible TRUE or FALSE depending on if all tests evaluated to TRUE or not.
#' Or \emph{'results'} which returns a named vector of all results.
#' @param mode Character value. The type of which the input must be.
#' @param ref Character value. Input reference for the error message.
#' If set to NULL the value of \code{x} is evaluated via non standard evalulation.
#' @param of.length Numeric value. Denotes that the vector has to be of a certain length.
#' Holds priority over \code{min.length} and \code{max.length} - if not set to NULL the letter
#' two are ignored.
#' @param min.length,max.length Numeric value. Denotes that the vector has to be
#' of certain minimal and/or maximal length.
#' @param skip.allow Logical. Allows the function to be skipped if \code{x} and
#' \code{skip.val} are identical.
#' @param skip.val The value that \code{x} needs to be equal to in order for the check
#' to be skipped.
#' @param verbose Logical value. Indicates whether any kind of feedback is supposed to
#' be given. \code{verbose} set to FALSE shuts down any error, warning or general messages
#' and results in the functions returning what is specified in \code{return}.
#' @inherit give_feedback params
#'
#' @return An invisible TRUE or an informative error message.
#' @export
#'
#' @examples # Not run:
#'
#'  vec1 <- c(1,2),
#'  vec2 <- c(1,2,3,4,5)
#'
#'  is_vec(x = vec1, mode = "numeric", of.length = 2)
#'
#'  are_vectors(c("vec1", "vec2"), mode = "numeric", min.length = 2)
#'
#'
#'

is_value <- function(x,
                     mode,
                     ref = NULL,
                     fdb.fn = "stop",
                     verbose = TRUE,
                     skip.allow = FALSE,
                     skip.val = NULL,
                     with.time = FALSE){

  if(base::isTRUE(skip.allow) && base::identical(x, skip.val)){

    base::invisible(TRUE)

  } else {

    if(base::is.null(ref)){ ref <- base::substitute(x)}

    msg <- NULL

    if(!base::length(x) == 1 ||
       !base::is.vector(x, mode = mode)){

      msg <- glue::glue("Input '{ref}' must be a {mode} value.")

    }

    # give feedback
    if(base::isFALSE(verbose)){fdb.fn <- "message"}

    give_feedback(
      fdb.fn = fdb.fn,
      msg = msg,
      verbose = verbose,
      with.time = with.time)

    return_value <-
      base::ifelse(test = base::is.null(msg), yes = TRUE, no = FALSE)

    return(return_value)

  }

}

#' @rdname is_value
#' @export
is_vec <- function(x,
                   mode,
                   ref = NULL,
                   of.length = NULL,
                   min.length = NULL,
                   max.length = NULL,
                   skip.allow = FALSE,
                   skip.val = NULL,
                   fdb.fn = "stop",
                   verbose = TRUE,
                   with.time = FALSE){

  if(base::isTRUE(skip.allow) && base::identical(x, skip.val)){

    base::invisible(TRUE)

  } else {

    # refer to input in feedback
    if(base::is.null(ref)){ ref <- base::substitute(x) }

    # default if all requirements are satisfied
    msg <- NULL

    # logical value indicating if the length is to be checked
    length_requirements_given <-
      base::any(c(!base::is.null(min.length), !base::is.null(max.length), !base::is.null(of.length)))

    # check requirements and prepare feedback
    if(base::isTRUE(length_requirements_given)){

      if(!base::is.null(of.length)){

        ref_length <- stringr::str_c(" of length ", of.length, sep = "")

      } else {

        ref_min_length <-
          base::ifelse(test = base::is.null(min.length),
                       yes = "",
                       no = stringr::str_c(" of min. length ", min.length, sep = "")
          )

        ref_max_length <-
          base::ifelse(test = base::is.null(max.length),
                       yes = "",
                       no = stringr::str_c(" of max. length ", max.length, sep = "")
          )

        # connect with 'and' if both requirements are given
        ref_connect <-
          base::ifelse(test = base::sum(c(!base::is.null(min.length), !base::is.null(max.length))) != 2,
                       yes = "",
                       no = " and ")

        ref_length <-
          glue::glue("{ref_min_length}{ref_connect}{ref_max_length}")

      }

    } else {

      ref_length <- ""

    }

    # check input vector and assemble feedback
    if(!base::is.vector(x, mode = mode)){

      msg <- glue::glue("Input '{ref}' must be a {mode} vector{ref_length}.")

    } else if(base::isTRUE(length_requirements_given)){

      if(!base::is.null(min.length) && !base::length(x) >= min.length){

        msg <- glue::glue("Input '{ref}' must be a {mode} vector{ref_length}.")

      } else if(!base::is.null(max.length) && !base::length(x) <= max.length){

        msg <- glue::glue("Input '{ref}' must be a {mode} vector{ref_length}.")

      } else if(!base::is.null(of.length) && !base::length(x) == of.length){

        msg <- glue::glue("Input '{ref}' must be a {mode} vector{ref_length}.")

      }

    }

    # give feedback
    if(base::isFALSE(verbose)){fdb.fn <- "message"} # verbose = FALSE and fdb.fn = "message" -> no feedback

    give_feedback(
      fdb.fn = fdb.fn,
      msg = msg,
      verbose = verbose,
      with.time = with.time
      )

    return_value <-
      base::ifelse(test = base::is.null(msg), yes = TRUE, no = FALSE)

    return(return_value)

  }

}

#' @rdname is_value
#' @export
are_values <- function(...,
                       mode,
                       fdb.fn = "stop",
                       verbose = TRUE,
                       with.time = FALSE,
                       skip.allow = FALSE,
                       skip.val = NULL,
                       return = "boolean"){

  # temporarily disabled due to weird bugs
  if(FALSE){

    input <- c(...)

    base::stopifnot(base::is.character(input))

    ce <- rlang::caller_env()

    results <-
      purrr::map(.x = input, .f = ~ rlang::parse_expr(.x) %>% base::eval(envir = ce)) %>%
      purrr::set_names(nm = input) %>%
      purrr::imap(.f = purrr::quietly(

        ~ confuns::is_value(
          x = .x,
          ref = .y,
          mode = mode,
          fdb.fn = "message",
          verbose = verbose,
          with.time = with.time,
          skip.allow = skip.allow,
          skip.val = skip.val
        )

      )
      ) %>%
      purrr::set_names(nm = input)

    # keep as valid if the fdb.fn slot is an empty character (=> no feedback equals valid input)
    valid_inputs <-
      purrr::map_lgl(
        .x = results,
        .f = ~ base::identical(.x[["messages"]], base::character(0))
      )

    # extract the feedback messages of the invalid inputs
    msg <-
      purrr::map(.x = results[!valid_inputs], .f = ~ .x[["messages"]]) %>%
      glue_list_report(
        lst = .,
        separator = NULL,
        combine_via = " \n"
      )

    if(base::length(msg) >= 1){

      give_feedback(
        msg = msg,
        verbose = verbose,
        fdb.fn = fdb.fn,
        with.time = FALSE)

    }

    # extrac the boolean return values of the actual check
    results <-
      purrr::map_lgl(.x = results, .f = ~ .x[["result"]])

    if(base::all(results == TRUE)){

      boolean <- base::invisible(TRUE)

    } else {

      boolean <- base::invisible(FALSE)

    }

    if(return == "boolean"){

      return(boolean)

    } else if(return == "results"){

      return(results)

    }

  }


  return(TRUE)

}

#' @rdname is_value
#' @export
are_vectors <- function(...,
                        mode,
                        fdb.fn = "stop",
                        verbose = TRUE,
                        with.time = FALSE,
                        of.length = NULL,
                        min.length = NULL,
                        max.length = NULL,
                        skip.allow = FALSE,
                        skip.val = NULL,
                        return = "boolean"){

  if(FALSE){

    input <- c(...)

    base::stopifnot(base::is.character(input))

    ce <- rlang::caller_env()

    results <-
      purrr::map(.x = input, .f = ~ base::parse(text = .x) %>% base::eval(envir = ce)) %>%
      purrr::set_names(nm = input) %>%
      purrr::imap(.f =  purrr::quietly(

        ~ confuns::is_vec(
          x = .x,
          ref = .y,
          mode = mode,
          fdb.fn = "message",
          verbose = verbose,
          with.time = with.time,
          of.length = of.length,
          min.length = min.length,
          max.length = max.length,
          skip.allow = skip.allow,
          skip.val = skip.val
        )

      )
      ) %>%
      purrr::set_names(nm = input)

    # keep as valid if the fdb.fn slot is an empty character (=> no feedback equals valid input)
    valid_inputs <-
      purrr::map_lgl(
        .x = results,
        .f = ~ base::identical(.x[["messages"]], base::character(0))
      )

    # extract the feedback messages of the invalid inputs
    msg <-
      purrr::map(.x = results[!valid_inputs], .f = ~ .x[["messages"]]) %>%
      glue_list_report(
        lst = .,
        separator = NULL,
        combine_via = " \n"
      )

    if(base::length(msg) >= 1){

      give_feedback(
        msg = msg,
        verbose = verbose,
        fdb.fn = fdb.fn,
        with.time = FALSE)

    }

    # extrac the boolean return values of the actual check
    results <-
      purrr::map_lgl(.x = results, .f = ~ .x[["result"]])

    if(base::all(results == TRUE)){

      boolean <- TRUE

    } else {

      boolean <- FALSE

    }

    if(return == "boolean"){

      return(boolean)

    } else if(return == "results"){

      return(results)

    }


  }

  return(TRUE)

}



# -----


# lazy check --------------------------------------------------------------

#' @title Check assign input
#'
#' @param assign Logical. If set to TRUE a named list will be assigned to the global
#' environment. This list contains data and information to rebuild or additionally
#' customize the output plot of this function.
#' @param assign_name The name the assigned list is supposed to have specified as
#' a single character value.
#'
#' @inherit lazy_check_dummy description details return
#' @export

check_assign <- function(assign = FALSE,
                         assign_name = character(1)){

  confuns::is_value(assign, mode = "logical")

  if(base::isTRUE(assign)){

    confuns::is_value(assign_name, mode = "character")

    if(assign_name == ""){

      base::stop("Argument 'assign_name' must not be ''.")

    }

    if(base::exists(x = assign_name, where = .GlobalEnv)){

      base::stop(stringr::str_c("It already exists an object named '",
                                assign_name, "' in the global environment.",
                                sep = ""))

    }

  }

  return(TRUE)

}




#' @title Check no overlap
#'
#' @param x,y Input vectors whoose content is to be checked.
#'
#' @return Error message if overlap is found. TRUE is no overlap is found.

check_no_overlap <- function(x, y, fdb.fn = "stop", with.time = FALSE){

  ref_x <- base::substitute(x)
  ref_y <- base::substitute(y)

  overlap <- base::intersect(x, y)

  if(base::length(overlap) >= 1){

    msg <-
      glue::glue(
        "Overlap is not allowed. {ref1} '{ref_overlap}' {ref2} part of input for argument '{ref_x}' and argument '{ref_y}'.",
        ref1 = adapt_reference(overlap, sg = "Value", pl = "Values"),
        ref2 = adapt_reference(overlap, sg = "is", pl = "are"),
        ref_overlap = glue::glue_collapse(x = overlap, sep = "', '", last = "' and '")
      )

    give_feedback(
      msg = msg,
      fdb.fn = fdb.fn,
      with.time = with.time
    )

  } else {

    return(TRUE)

  }

}

#' @title Check data.frame validity
#'
#' @description Checks whether the input data.frame contains variables
#' of certain classes and names.
#'
#' @param df A data.frame.
#' @param var.class A named list. The names have to match the
#' variable names of the data.frame that are to be validated. The respective
#' elements specify the class the data.frame variable must have specified
#' as character strings.
#' @inherit is_value params
#' @inherit give_feedback params
#'
#' @return An informative message, warning or error or TRUE if valid.
#' @export
#'
#' @examples
#'  # make sure that the input data.frame has
#'  # the numeric variables 'mpg' and 'cyl'.
#'
#'  check_data_frame(df = mtcars,
#'                   var.class = list(mpg = "numeric",
#'                                    cyl = "numeric"))

check_data_frame <- function(df,
                             var.class = list(),
                             ref = NULL,
                             verbose = TRUE,
                             with.time = FALSE,
                             fdb.fn = "stop"){

  # denote input reference
  if(base::is.null(ref)){

    ref_input <- base::substitute(df)

  } else {

    ref_input <- ref

  }

  # assemble report if anything is invalid

  all_names <- base::names(df)

  report <- base::vector(mode = "list")

  for(name in base::names(var.class)){

    ref_name <- stringr::str_c("Variable '", name, "'", sep = "")

    if(!name %in% all_names){

      report[[ref_name]] <- "is missing."

    } else if(!is_any_of(input = df[[name]], valid.classes = var.class[[name]])){

        report[[ref_name]] <-
          glue::glue(
            "must be of class '{ref_valid_classes}' but is of {ref1} '{ref_current_class}'.",
            ref_valid_classes = glue::glue_collapse(var.class[[name]], sep = "', '", last = "' or '"),
            ref1 = adapt_reference(base::class(df[[name]]), sg = "class", pl = "classes"),
            ref_current_class = glue::glue_collapse(base::class(df[[name]]), sep = ", ", last = "' and '")
          ) %>%
          base::as.character()

    }

  }

  # return report if anything is invalid else return TRUE
  if(base::length(report) >= 1){

    msg_init <- glue::glue("\n\nProblematic data.frame input for argument '{ref_input}':\n\n")

    msg_report <-
      glue_list_report(
        lst = report,
        separator = " ",
        combine_via = "\n"
      )

    msg <- glue::glue("{msg_init}{msg_report}")

    confuns::give_feedback(
      msg = msg,
      fdb.fn = fdb.fn,
      with.time = with.time,
      verbose = verbose
    )

    base::invisible(FALSE)

  } else {

    return(TRUE)

  }

}

#' @title Check directory input
#'
#' @param directories Character vector. Directories to check.
#' @param type Character value. One of \emph{'files', 'folders', 'create_files'}. Checks
#' whether the given directories lead to the specified type or are creatable.
#' @inherit is_value params
#' @inherit give_feedback params
#'
#' @return An informative error message or an invisible TRUE.
#' @export

check_directories <- function(directories,
                              ref = NULL,
                              type = "folders",
                              fdb.fn = "stop",
                              with.time = FALSE,
                              verbose = TRUE){

  is_vec(directories, mode = "character", "directories")
  is_value(ref, mode = "character", skip.allow = TRUE, skip.val = NULL)
  is_value(type, mode = "character")

  if(base::is.null(ref)){

    ref_input <-
      glue::glue(
        "specified as input for argument '{ref_arg}'",
        ref_arg = base::substitute(directories)
      )

  } else {

    ref_input <- ref

  }


  base::stopifnot(type %in% c("files", "folders", "create_files"))

  msg <- NULL

  if(type %in% c("files", "folders")){

    not_found <-
      purrr::map(.x = directories,
                 .f = function(dir){

                   check_fun <-
                     base::ifelse(type == "files", base::file.exists, base::dir.exists)

                   if(!check_fun(dir)){

                     return(dir)

                   } else {

                     return(NULL)

                   }}) %>%
      purrr::discard(.p = base::is.null) %>%
      base::unlist(use.names = FALSE)

    if(!base::is.null(not_found) && base::is.character(not_found)){

      type2 <- c("files", "folders")[!c("files", "folders") %in% type]

      msg <-
        glue::glue(
          "The following {ref1} {ref_input} {ref2} not exist or {ref3} to {ref4}{ref5} instead of {ref6}{ref7}: \n- {ref_not_found}",
          ref1 = adapt_reference(not_found, sg = "directory", pl = "directories"),
          ref2 = adapt_reference(not_found, sg = "does", pl = "do"),
          ref3 = adapt_reference(not_found, sg = "leads", pl = "lead"),
          ref4 = adapt_reference(not_found, sg = "a ", pl = ""),
          ref5 = adapt_reference(not_found, sg = stringr::str_remove(type2, "s$"), pl = type2),
          ref6 = adapt_reference(not_found, sg = "a ", pl = ""),
          ref7 = adapt_reference(not_found, sg = stringr::str_remove(type, "s$"), pl = type),
          ref_not_found = glue::glue_collapse(not_found, sep = "\n- ")
        )

      confuns::give_feedback(
        msg = msg,
        fdb.fn = fdb.fn,
        with.time = with.time,
        verbose = verbose
      )

    }

  } else if(type == "create_files") {

    not_creatable <-
      purrr::keep(.x = directories, .p = function(dir){

        if(base::file.exists(dir)){

          return(TRUE)

        } else {

          res <-
            base::isFALSE(base::file.create(dir, showWarnings = FALSE))

          if(base::isTRUE(res)){base::file.remove(dir)}

          return(res)

        }

      })

    if(base::length(not_creatable) >= 1){

      msg <-
        glue::glue(
          "Attempting to create {ref1} '{ref_dir}' did not work. Do all subfolders of the specified {ref1} exist? Does the file or folder already exist?",
          ref1 = adapt_reference(not_creatable, sg = "directory", pl = "directories"),
          ref_dir = glue::glue_collapse(not_creatable, sep = "', '", last = "' and '")
          )

      confuns::give_feedback(
        msg = msg,
        fdb.fn = fdb.fn,
        verbose = verbose,
        with.time = with.time
      )

    }

  }

  if(base::is.null(msg)){

    return(TRUE)

  } else {

    return(FALSE)

  }

}

#' @title Check h and k input for hclust
#' @export

check_h_k <- function(h = NULL, k = NULL, only.one = FALSE, skip.allow = TRUE){

  if(base::all(base::is.null(k), base::is.null(h)) & base::isFALSE(skip.allow)){

    msg <- "Please specify either argument 'k/ks' or argument 'h/hs'."

    give_feedback(msg = msg, fdb.fn = "stop")

  }

  if(base::isTRUE(only.one)){

    if(base::all(base::is.numeric(k), base::is.numeric(h))){

      msg <- "Please specify only one of argument 'k' or argument 'h'. Not both."

      give_feedback(msg = msg, fdb.fn = "stop")

    }

  }

 are_vectors(c("k", "h"), mode = "numeric", skip.allow = TRUE, skip.val = NULL)

}

#' @title Overwrite check
#'
#' @description Make sure that nothing is overwritten without specifically allowing it.
#'
#' @inherit check_one_of params
#' @export
check_none_of <- function(input,
                          against,
                          ref.input = NULL,
                          ref.against,
                          overwrite = NULL,
                          fdb.fn = "stop",
                          with.time = FALSE,
                          force = NULL){

  if(base::is.null(ref.input)){

    ref.input <- "argument input"

  }

  if(base::isTRUE(overwrite) | base::isTRUE(force)){

    base::invisible(TRUE)

  } else {

    overlap <- against[against %in% input]

    if(base::length(overlap) >= 1){

      ref_overlap <- glue::glue_collapse(overlap, sep = "', '", last = "' and '")

      ref1 <- adapt_reference(overlap, sg = "Value", pl = "Values")

      ref2 <- adapt_reference(overlap, sg = "is", pl = "are")

      if(!base::is.null(overwrite)){

        ref_overwrite <- overwrite_hint

      } else {

        ref_overwrite <- ""

      }

      msg <- glue::glue("{ref1} '{ref_overlap}' of {ref.input} {ref2} already present in {ref.against}.{ref_overwrite}")

      give_feedback(msg = msg, fdb.fn = fdb.fn, with.time = with.time)

    } else {

      base::invisible(TRUE)

    }

  }

}

#' Check valid values
#'
#' @param input An input vector to be checked.
#' @param against A vector of valid inputs.
#' @param ref.input Character value or NULL. The reference for argument \code{input} input.
#'
#' @return An error message or an invisible TRUE if all values of input are valid.
#'
#' @details Error message is build via \code{glue::glue()} building the following
#' string:
#'
#' "Value/Values '\emph{invalid values}' of {ref.input} is/are invalid. Valid input options are: '{\emph{valid inputs}}'."
#'
#' @export
#'
check_one_of <- function(input,
                         against,
                         ref.input = NULL,
                         fdb.fn = "stop",
                         fdb.opt = 1,
                         ref.opt.2 = "valid input options",
                         suggest = TRUE,
                         verbose = TRUE,
                         with.time = FALSE,
                         in.shiny = FALSE,
                         ...){

  base::is.vector(input)
  base::is.vector(against)

  if(purrr::is_empty(against)){

    stop("`against` is empty.")

  }

  if(base::is.null(ref.input)){

    ref.input <-
      base::tryCatch({

        ref.input <-
          glue::glue("input for argument '{base::substitute(input)}'") %>%
          base::as.character()

      }, error = function(error){

        "input"

      })

    if(base::length(ref.input) == 2){ ref.input <- "input" }

  } else {

    if("glue" %in% base::class(ref.input)){

      ref.input <- base::as.character(ref.input)

    } else {

      is_value(ref.input, "character")

    }

  }

  if(base::length(ref.input) > 1){

    ref.input <- "input"

    }


  if(base::any(!input %in% against)){

    ref_against <- glue::glue_collapse(against, sep = "', '", last = "' and '")

    invalid <- input[!input %in% against]

    valid_input_options <-
      glue::glue("Valid input options are: '{ref_against}'") %>%
      base::as.character()

    if(base::isTRUE(suggest)){

      suggestions <-
        str_suggest_list(
          string = invalid,
          pool = against,
          ...
        )

      n_suggestions <-
        purrr::map(suggestions, .f = base::length) %>%
        base::unname()

      end_string <-
        purrr::discard(.x = suggestions, .p = ~ base::length(.x) == 0) %>%
        purrr::map(
          .f = ~
            scollapse(.x, sep = "', '", last = "' or '") %>%
            base::as.character() %>%
            wrap_strings(wrap.in = c("'", "'?"))
        ) %>%
        glue_list_report(
          separator = "' did you mean ",
          prefix = "Instead of '"
        ) %>%
        base::as.character()

      if(base::any(n_suggestions == 0)){

        end_string <- valid_input_options

      }

    } else {

      end_string <- valid_input_options

    }

    if(fdb.opt == 1){

      msg <-
        glue::glue(
          "{ref1} '{ref_invalid}' of {ref.input} {ref2} invalid. {end_string}.",
          ref1 = adapt_reference(invalid, sg = "Value", pl = "Values"),
          ref2 = adapt_reference(invalid, sg = "is", pl = "are"),
          ref_invalid = glue::glue_collapse(invalid, sep = "', '", last = "' and '"),

        )

    } else if(fdb.opt == 2) {

      msg <- glue::glue(
        glue::glue(
          "Did not find {ref1} '{ref_invalid}' of {ref.input} among {ref.opt.2}. {end_string}.",
          ref1 = adapt_reference(invalid, sg = "value", pl = "values"),
          ref_invalid = glue::glue_collapse(invalid, sep = "', '", last = "' and '")
        )
      )

    }

    confuns::give_feedback(
      msg = msg,
      fdb.fn = fdb.fn,
      with.time = with.time,
      verbose = verbose,
      in.shiny = in.shiny
    )

  } else {

    return(TRUE)

  }

}
# -----



# adjusting check ---------------------------------------------------------

#' @title Check across subset input
#' @export
check_across_subset_negate <- function(across, across.subset, all.groups){

  across_subset_input <- base::substitute(across.subset)

  # distinguish between groups to keep and groups to discard
  discard_groups <-
    stringr::str_subset(across.subset, pattern = "^-") %>%
    stringr::str_remove_all(pattern = "^-")

  keep_groups <-
    stringr::str_subset(across.subset, pattern = "^[^-]")

  # check for ambiguous input
  duplicated_groups <-
    base::intersect(keep_groups, discard_groups)

  if(base::length(duplicated_groups) >= 1){

    duplicated_groups <- stringr::str_c("(-)", duplicated_groups)

    msg <-
      glue::glue("Ambiguous values ('{duplicated_input}') in input for argument '{across_subset_input}'.",
                 duplicated_input = glue::glue_collapse(x = duplicated_groups, sep = "', ", last = "' and '"))

    give_feedback(fdb.fn = "stop", msg = msg, with.time = FALSE)

  }

  across.subset <- c(keep_groups, discard_groups)

  # keep valid groups
  check_one_of(
    input = across.subset,
    against = all.groups,
    ref.input = base::as.character(glue::glue("input to subset '{across}'-groups"))
  )

  #if no error all are valid
  across.subset_valid <- across.subset

  # keep valid distinguished groups
  discard_groups <- discard_groups[discard_groups %in% across.subset_valid]

  # in case only -across.subset has been provided "refill" 'keep_groups'
  if(base::length(keep_groups) == 0){

    keep_groups <- all.groups

  }

  # discard what has been denoted with -
  keep_groups <- keep_groups[!keep_groups %in% discard_groups]

  return(keep_groups)

}

#' @title Data.frame variable check
#'
#' @description Selects the variables denoted in
#' \code{keep} and \code{variables}. The letter ones
#' are checked for validity. If \code{variables} is
#' set to NULL all valid variables are kept.
#'
#' @param keep Character vector or NULL. If character, specifies variables
#' that are to be kept even if they are not of those classes denoted in
#' \code{valid.classes}. Variables specified like that are not included in
#' the pivoting process!
#'
#' @param ref_df Character value. Given to argument \code{ref} of
#' function \code{check_data_frame()}.
#'
#' @inherit argument_dummy params
#'
#' @return The input \code{df} with all selected variables.
#' @export
#'

check_df_variables <- function(df, valid.classes, variables = NULL, keep = NULL, ref_df = NULL, verbose = TRUE){

  # extract and check 'variables'
  if(base::is.null(variables) | base::any(stringr::str_detect(variables, pattern = "^-"))){

    res_df <-
      purrr::keep(.x = df, .p = ~ is_any_of(.x, valid.classes))

    discard_variables <-
      stringr::str_subset(variables, pattern = "^-") %>%
      stringr::str_remove(pattern = "^-")

    if(base::length(discard_variables) >= 1){

      check_one_of(
        input = discard_variables,
        against = base::colnames(res_df),
        ref.input = "variables to be discarded"
      )

      res_df <- dplyr::select(res_df, -dplyr::all_of(discard_variables))

    }

  } else {

    var.class <-
      purrr::map(.x = variables, .f = function(var){ valid.classes }) %>%
      purrr::set_names(nm = variables)

    check_data_frame(
        df = df,
        var.class = var.class,
        ref = ref_df
      )

    # if no error was thrown keep all variables
    res_df <- dplyr::select(df, dplyr::all_of(variables))

  }

  variables_kept <- base::colnames(res_df)

  # extract and check 'keep'
  if(base::is.character(keep) & !base::all(keep %in% variables_kept)){

    keep <- keep[!keep %in% variables_kept]

    var.class <-
      purrr::map(.x = keep, .f = function(i){ "any" }) %>%
      purrr::set_names(nm = keep)

    check_data_frame(
      df = df,
      var.class = var.class,
      ref = ref_df,
    )

    keep_df <-
      dplyr::select(df, dplyr::all_of(x = keep))

  } else {

    keep_df <- NULL

  }

  # if additional variables have been kept with argument 'keep'
  # add to the resulting data.frame
  if(base::is.data.frame(keep_df) & base::is.data.frame(res_df)){

    res_df <-
      base::cbind(keep_df, res_df)

  }

  return(res_df)

}


#' @title Check and adjust k
#' @return Numeric vector.
#' @export
#'
check_ks <- function(k.input, of.length = NULL){

  ref <- base::substitute(k.input)

  is_vec(x = k.input, ref = ref,  mode = "numeric", of.length = of.length)

  out <-
    base::as.integer(k.input) %>%
    base::unique() %>%
    base::sort()

  return(out)

}


#' @title Compare input to control input
#'
#' @description Compares the values of an input-vector against a control-vector containing
#' valid values and returns the values of input that were found among the valid ones.
#'
#' @param input A vector of any kind.
#' @param against A vector of the same kind as \code{input}.
#' @inherit verbose params
#' @inherit give_feedback params
#' @param ref.input The reference character value for input.
#' @param ref.against The reference character value for against.
#' @param ... Additional arguments given to \code{give_feedback()}.
#'
#' @return An informative error message about which elements of \code{input} were found in \code{against} or an invisible TRUE.
#'
#' @details If none of the input values are found an error is raised with the message:
#'
#'   glue::glue("Did not find any element of \code{ref.input} in \code{ref.against}.")
#'
#'   If only some of the input values are found the function denoted in \code{fdb.fn} is called with the message:
#'
#'   glue::glue("Of \code{ref.input} did not find 'missing' in \code{ref.against}.")
#'
#' @export

check_vector <- function(input,
                         against,
                         ref.input = "input vector",
                         ref.against = "valid options",
                         ref.connect = "among",
                         fdb.fn = "message",
                         verbose = TRUE,
                         with.time = FALSE){

  base::stopifnot(base::is.vector(input) & base::is.vector(against))
  base::stopifnot(base::class(input) == base::class(against))

  found <- against[against %in% input]
  missing <- input[!input %in% against]

  if(base::isTRUE(verbose) && base::length(missing) != 0){

    missing <- stringr::str_c(missing, collapse = "', '")

  }

  if(base::length(found) == 0){

    msg <-
      glue::glue("Did not find any element of {ref.input} {ref.connect} {ref.against}.")

    confuns::give_feedback(
      msg = msg,
      fdb.fn = "stop",
      with.time = with.time
    )

  } else if(base::length(missing) != 0){

      msg <-
        glue::glue(
          "Of {ref.input} did not find '{missing}' {ref.connect} {ref.against}.",
          missing = glue::glue_collapse(missing, sep = "', '", last = "' and '"))

      give_feedback(
        msg = msg,
        fdb.fn = fdb.fn,
        verbose = verbose,
        with.time = with.time
      )

  }

  return(input[input %in% found])

}
kueckelj/confuns documentation built on June 28, 2024, 9:19 a.m.