R/asserts.R

Defines functions dots2argNames ensureValidName ensureOption assertInRange assertOption assertPositiveSemiDefinite ensureMatrix ensureLength assertProbability assertPositive assertDescending assertAscending assertNotInVector assertNotInDataTable assertInDataTable assertUnique assertValue assertNumericMatrix assertFactor assertInteger assertNumeric assertNotNull assertType assertClass assertAtLeast assertAtLeastLength assertLength assertNotEqual assertEqual assertLengthEqual assertNotMissing

#' Are arguments missing?
#'
#' @param ... any number of missing(arg) as named elements
#' e.g. x = missing(x)
#' @noRd
assertNotMissing <- function(..., call = sys.call(-1)) {
  args <- list(...)
  names <- names(args)
  args <- unlist(args)

  stopifnot(length(args) == length(names))

  if (any(args)) {
    argMissingError(names[args], call = call)
  }
}

#' Are Lengths Equal?
#'
#' @description Checks if all passed vars are of equal Length. Caveat:
#' length(matrix) = number of elements but length(data.frame) = number
#'  of columns.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertLengthEqual <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  stopifnot(length(dots$args) >= 2)

  sameLength <- length(unique(lengths(dots$args))) == 1L
  if (!sameLength) {
    lengthError(dots$names, "equal", call = call)
  }
}

#' Are arguments equal to value?
#'
#' @description Checks if all passed vars are equal to given value.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @param val Value to check if variables are equal to
#' @noRd
assertEqual <- function(..., val, call = sys.call(-1)) {
  dots <- dots2argNames(...)

  notEqual <- !sapply(dots$args, function(i) {
    i == val
  })
  if (any(notEqual)) {
    notEqualError(dots$names, val, call = call)
  }
}

#' Are arguments not equal to value?
#'
#' @description Checks if all passed vars are not equal to given value.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @param val Value to check if variables are equal to
#' @noRd
assertNotEqual <- function(..., val, call = sys.call(-1), msg="") {
  dots <- dots2argNames(...)
  
  Equal <- !sapply(dots$args, function(i) {
    i != val
  })
  if (any(Equal)) {
    equalError(dots$names, val, call = call, msg)
  }
}

#' Is length correct?
#'
#' @description Checks if all passed vars are of length 'length'. Caveat:
#' length(matrix) = numer of elements but length(data.frame) = number
#'  of columns.
#' @param length Length to check
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertLength <- function(..., length, call = sys.call(-1)) {
  dots <- dots2argNames(...)
  correctLength <- lengths(dots$args) == length
  if (!all(correctLength)) {
    lengthError(dots$names[!correctLength],
      prop = length,
      msg = "{ names *} should be of length { prop }!", call = call
    )
  }
}

#' Is length correct?
#'
#' @description Checks if all passed vars are of at least
#' of length 'length'. Caveat: length(matrix) = numer of elements
#' but length(data.frame) = number of columns.
#' @param length Length to check
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertAtLeastLength <- function(..., length, call = sys.call(-1)) {
  dots <- dots2argNames(...)
  correctLength <- lengths(dots$args) >= length
  if (!all(correctLength)) {
    lengthError(dots$names[!correctLength],
      prop = length,
      msg = "{ names *} should be at least length { prop }!", call = call
    )
  }
}

#' Are all elements greater than or equal to a value?
#'
#' @description Checks if passed vector is >= a minimum value
#' @param vec Vector under consideration
#' @noRd
assertAtLeast <- function(..., minVal, call = sys.call(-1)) {
  
  dots <- dots2argNames(...)
  correctSize <- dots$args >= minVal
  
  if (!all(correctSize)) {
    minError(dots$names[!correctSize], minVal, call = call)
  }
}

#' Check for Class
#'
#' @description Checks if all passed vars inherit from class.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @param class Class to check against.
#' @noRd
assertClass <- function(..., class, call = sys.call(-1)) {
  dots <- dots2argNames(...)
  wrongClass <- !sapply(dots$args, inherits, class)

  if (any(wrongClass)) {
    classError(dots$names[wrongClass], class = class, call = call)
  }
}

#' Check for Type
#'
#' @description Checks if all passed vars and their content are of type.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @param type Type to check against.
#' @param deep Should the elements of the variable be tested?
#' @noRd
assertType <- function(..., type, deep = TRUE, call = sys.call(-1)) {
  dots <- dots2argNames(...)
  reduceType <- function(arg) {
    if (deep && length(arg) >= 1) {
      types <- sapply(arg, typeof)
      if (length(types) == 1) {
        return(types == type)
      }
      return(Reduce(`&&`, types == type))
    } else {
      typeof(arg) == type
    }
  }
  wrongType <- !sapply(dots$args, reduceType)
  if (any(wrongType)) {
    typeError(dots$names[wrongType], type = type, call = call)
  }
}

#' Check for Numeric
#'
#' @description Checks if all passed vars and their content are not null
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertNotNull <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  isNull <- sapply(dots$args, function(x) is.null(x))
  if (any(isNull)) {
    nullError(dots$names[isNull], type = "numeric", call = call)
  }
}

#' Check for Numeric
#'
#' @description Checks if all passed vars and their content are numeric.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertNumeric <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  reduceNumeric <- function(arg) {
    types <- sapply(arg, typeof)
    Reduce(`&&`, types == "integer" |
      types == "double" | types == "numeric")
  }
  notNumeric <- !sapply(dots$args, reduceNumeric)
  if (any(notNumeric)) {
    typeError(dots$names[notNumeric], type = "numeric", call = call)
  }
}

#' Check for Integer
#'
#' @description Checks if all passed vars and their content are integers or can
#' be coerced to integer without loss of information.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertInteger <- function(..., type, call = sys.call(-1)) {
  assertNumeric(..., call = call)
  dots <- dots2argNames(...)
  checkInteger <- function(arg) {
    arg <- unlist(arg)
    all(arg == as.integer(arg))
  }
  notInteger <- !sapply(dots$args, checkInteger)
  if (any(notInteger)) {
    typeError(dots$names[notInteger], type = "integer", call = call)
  }
}

#' Check for Factor
#'
#' @description Checks if all passed vars and their content are factors.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertFactor <- function(..., type, call = sys.call(-1)) {
  # assertNumeric(..., call = call)
  dots <- dots2argNames(...)
  notFactor <- !sapply(dots$args, function(fac) {
    is.factor(fac)
  })
  if (any(notFactor)) {
    typeError(dots$names[notFactor], type = "factor", call = call)
  }
}

#' Check for numeric matrix (character matrices not allowed)
#'
#' @description Checks if all passed vars and their content are numeric matrices
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertNumericMatrix <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  notNumericMatrix <- !sapply(dots$args, function(mat) {
    isMatrix <- is.matrix(mat)
    isNumeric <- is.numeric(c(mat))
    isMatrix & isNumeric
  })
  if (any(notNumericMatrix)) {
    typeError(dots$names[notNumericMatrix], type = "numeric matrix", call = call)
  }
}

#' Check for Value
#'
#' @description Checks if all passed vars have a value other than NULL and NA
#' and a length > 0.
#' Does not check if all elements contained are NULL or NA.
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @noRd
assertValue <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  noValue <- sapply(dots$args, is.null) |
    is.na(dots$args) |
    lengths(dots$args) == 0

  if (any(noValue)) {
    noValueError(dots$names[noValue], call = call)
  }
}

#' Are Values Unique?
#'
#' @description Checks if all passed vars have only unique values.
#' @param ... Any number of list or vectors as named elements e.g. var1 = var1.
#' @noRd
assertUnique <- function(..., call = sys.call(-1)) {
  dots <- dots2argNames(...)
  stopifnot(sapply(dots$args, is, "vector") |
    sapply(dots$args, is, "list") | sapply(dots$args, is, "glue") |
    lengths(dots$args) == 1L)

  isUnique <- function(var) {
    length(var) == length(unique(var))
  }
  notUnique <- !sapply(dots$args, isUnique)
  if (any(notUnique)) {
    notUniqueError(dots$names[notUnique], call = call)
  }
}

#' Var Defined?
#'
#' @description Checks if all passed vars have been defined in dt.
#' @param vars Name of variables to check.
#' @param dt data.table to check for vars. Can also be character vector with
#' defined variables.
#' @noRd
assertInDataTable <- function(vars, dt, call = sys.call(-1)) {
  if (is.data.frame(dt)) {
    dtNames <- names(dt)
  } else {
    stopifnot(is.character(dt))
    dtNames <- dt
  }
  notDefined <- !vars %in% dtNames

  if (any(notDefined)) {
    notDefinedError(vars[notDefined], call = call)
  }
}

#' Var Not Defined?
#'
#' @description Checks if all passed vars have not been defined in dt.
#' @param ... vars Name of variables to check.
#' @param dt data.table to check for vars. Can also be character vector with
#' defined variables.
#' @noRd
assertNotInDataTable <- function(vars, dt, call = sys.call(-1)) {
  if (is.data.frame(dt)) {
    dtNames <- names(dt)
  } else {
    stopifnot(is.character(dt))
    dtNames <- dt
  }
  areDefined <- vars %in% dtNames

  if (any(areDefined)) {
    alreadyDefinedError(vars[areDefined], call = call)
  }
}

#' Check if single string not in vector of strings
#'
#' @description Checks if all passed vars have not been defined in dt.
#' @param ... vars Name of variables to check.
#' @param dt data.table to check for vars. Can also be character vector with
#' defined variables.
#' @noRd
assertNotInVector <- function(var, vec, call = sys.call(-1)) {
  areInVector <- var %in% vec
  elementOfVar <- var[areInVector]
  nameVar <- deparse(substitute(var))
  nameVec <- deparse(substitute(vec))

  if (any(areInVector)) {
    alreadyInVectorError(elementOfVar, nameVar, nameVec, call = call)
  }
}

#' Is vector in ascending order?
#'
#' @description Checks if passed vector is in ascending order
#' @param vec Vector under consideration
#' @noRd
assertAscending <- function(vec, call = sys.call(-1)) {
  name <- deparse(substitute(vec))
  ascending <- all(vec[order(vec)] == vec) & (length(unique(vec)) == length(vec))

  if (!ascending) {
    orderError(name, "ascending", call = call)
  }
}

#' Is vector in descending order?
#'
#' @description Checks if passed vector is in descending order
#' @param vec Vector under consideration
#' @noRd
assertDescending <- function(vec, call = sys.call(-1)) {
  name <- deparse(substitute(vec))
  descending <- all(vec[rev(order(vec))] == vec) & (length(unique(vec)) == length(vec))

  if (!descending) {
    orderError(name, "descending", call = call)
  }
}


#' Are all elements of vector positive?
#'
#' @description Checks if passed vector is includes only positive values
#' @param vec Vector under consideration
#' @noRd
assertPositive <- function(vec, call = sys.call(-1)) {
  name <- deparse(substitute(vec))
  positive <- all(vec > 0)

  if (!positive) {
    signError(name, "positive", call = call)
  }
}

#' Are all elements of vector probabilities?
#'
#' @description Checks if passed vector includes only proper probabilities
#' @param vec Vector under consideration
#' @noRd
assertProbability <- function(vec, call = sys.call(-1)) {
  name <- deparse(substitute(vec))
  probability <- all(vec >= 0 & vec <= 1)

  if (!probability) {
    probError(name, call = call)
  }
}



#' Ensure Length
#'
#' @description Ensures that var is of length n or 1. Repeats the value n
#' times in the second case. Throws simstudy::lengthError if other length found.
#' @param ... One variable as named element: var = var.
#' @param n Desired length.
#' @return Invisibly returns input var with length n.
#' @noRd
ensureLength <- function(..., n,
                         msg = list(
                           "{ dots$names[[1]] } should be",
                           " either length 1 or { n } but",
                           " is { length(var) }!"
                         ),
                         call = sys.call(-1)) {
  dots <- dots2argNames(...)
  stopifnot(length(dots$args) == 1, !missing(n))
  var <- dots$args[[1]]

  if (length(var) == 1) {
    invisible(rep(var, n))
  } else if (length(var) == n) {
    invisible(var)
  } else {
    lengthError(
      names = dots$names, call = call,
      msg = do.call(glue, msg)
    )
  }
}

#' Ensure Input is Matrix
#'
#' @description Checks if var is a matrix or vector, if vector converts it to 1
#' row matrix.
#' @param var Variable to check
#' @return var as matrix.
#' @importFrom methods is
#' @noRd
ensureMatrix <- function(var) {
  stopifnot(is(var, "matrix") || is(var, "vector"))

  if (is(var, "matrix")) {
    return(var)
  }

  if (is(var, "vector")) {
    return(matrix(var, nrow = 1))
  }
}

#' Check Matrix is Positve Definite
#'
#' @description Checks if Matrix is positiv definite,
#' @param ... A matrix as named element e.g. var1 = var1.
#' @noRd
assertPositiveSemiDefinite <- function(..., call = sys.call(-1)) {
  stopifnot(...length() == 1)
  dots <- dots2argNames(...)
  matrix <- dots$args[[1]]
  isSym <- isSymmetric(round(matrix, 7))
  eigenValues <- round(unlist(eigen(matrix, only.values = TRUE)), 8)

  if (!all(eigenValues >= 0) || !isSym) {
    notPositiveSemiDefiniteError(dots$names, call = call)
  }
}

#' Check Option Valid
#'
#' @param ... An argument as named element e.g. var1 = var1.
#' @param value Value of the argument
#' @param options Valid options for the argument
#' @param msg Additonal message to be displayed.
#' @param call sys.call to pass on to the error.
#' @noRd
assertOption <- function(..., options, msg = "", call = sys.call(-1)) {
  stopifnot(...length() == 1)
  dots <- dots2argNames(...)
  notValid <- !dots$args[[1]] %in% options

  if (notValid) {
    optionInvalidError(
      name = dots$names[[1]],
      value = dots$args[[1]],
      options = options,
      msg = msg,
      call = call
    )
  }
}

#' Check Values in Range
#' !! Slow for a larg number of elements !!
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @param range Numeric vector of range as c(min,max).
#' @param minCheck Comparison that is made with the lower boundary.
#' @param maxCheck Comparison that is made with the upper boundary.
#' @noRd
assertInRange <- function(...,
                          range,
                          minCheck = ">=",
                          maxCheck = "<=",
                          call = sys.call(-1)) {
  assertLength(
    minCheck = minCheck, maxCheck = maxCheck,
    length = 1,
    call = call
  )
  assertLength(range = range, length = 2)
  assertNumeric(range = range)
  assertOption(
    minCheck = minCheck,
    options = c("<", "<=", "==", "!=", ">", ">=")
  )
  assertOption(
    maxCheck = maxCheck,
    options = c("<", "<=", "==", "!=", ">", ">=")
  )
  dots <- dots2argNames(...)
  do.call(function(...) assertNumeric(..., call = call), dots$args)

  createExpressions <- function(values) {
    glue(
      "{values} {minCheck} {range[[1]]}",
      " && {values} {maxCheck} {range[[2]]} "
    )
  }

  inRange <- function(values) {
    all(
      sapply(
        lapply(createExpressions(values), function(x) parse(text = x)),
        eval
      )
    )
  }
  
  dots$args <- lapply(dots$args, range)

  notInRange <- !sapply(dots$args, inRange)

  if (any(notInRange)) {
    valueError(
      names = dots$names[notInRange], var = range,
      msg = list(
        "Some values in {names *} are not in the",
        " range from {var[[1]]} to {var[[2]]}."
      ),
      call = call
    )
  }
}

#' Ensure Option Valid
#'
#' @param ... An argument as named element e.g. var1 = var1.
#' @param value Value of the argument
#' @param options Valid options for the argument
#' @param default Value argument will default to.
#' @param call sys.call to pass on to the warn.
#' @return The argument or default.
#' @noRd
ensureOption <- function(..., options, default, call = sys.call(-1)) {
  stopifnot(...length() == 1)
  dots <- dots2argNames(...)
  notValid <- !dots$args[[1]] %in% options

  if (notValid) {
    optionInvalidWarning(
      dots$names[[1]],
      dots$args[[1]],
      options,
      default,
      call
    )
    return(default)
  }
  dots$args[[1]]
}

#' Ensure Names are Valid
#'
#' @param names A character vector of names to check.
#' @param call sys.call to pass on to the warn.
#' @return The modified names.
#' @noRd
ensureValidName <- function(names, call = sys.call(-1)) {
  notValid <- !.isValidVarName(names)

  if (any(notValid)) {
    # TODO pluralize
    valueWarning(
      msg = list(
        "Variable name(s) '{var *}' not a valid R variable name,",
        "and will be converted to: '{make.names(var)}'."
      ), var = names[notValid],
      call. = call
    )
    return(make.names(names))
  }

  names
}

#' Dots to Args & Names
#'
#' @param ... Any number of variables as named elements e.g. var1 = var1.
#' @return A list containing the arguments and names.
#' @noRd
dots2argNames <- function(...) {
  stopifnot(...length() != 0)
  args <- list(...)
  names <- names(args)
  names <- names[names != ""]
  stopifnot(length(args) == length(names))

  list(args = args, names = names)
}

Try the simstudy package in your browser

Any scripts or data that you put into this service are public.

simstudy documentation built on Nov. 23, 2023, 1:06 a.m.