is_flag <- function(x, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  is.logical(x) && length(x) == 1L && !is.na(x)
}

is_character <- function(x, n_chars = NULL, pattern = NULL, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  ok <- is.character(x) && !anyNA(x) && length(x) > 0L
  if (ok && !is.null(n_chars)) {
    ok <- all(nchar(x) %in% n_chars)
  }
  if (ok && !is.null(pattern)) {
    ok <- all(grepl(pattern, x))
  }
  ok
}

is_string <- function(x, ..., n_chars = NULL, pattern = NULL, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  ok <- is.character(x) && length(x) == 1L && !is.na(x)
  if (ok && !is.null(n_chars)) {
    ok <- nchar(x) %in% n_chars
  }
  if (ok && !is.null(pattern)) {
    ok <- grepl(pattern, x, ...)
  }
  ok
}

is_count <- function(x, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  is.numeric(x) && length(x) == 1L && is.finite(x) && trunc(x) == x && x > 0L
}

is_number <- function(x, lower = -Inf, upper = Inf, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  is.numeric(x) && length(x) == 1L && is.finite(x) && x >= lower && x <= upper
}

is_version <- function(x, n_digits, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  if (is_number(x)) {
    x <- as.character(x)
  }
  is_string(x, n_chars = n_digits, pattern = "^[0-9]+$")
}

is_dateish <- function(x, null_ok = FALSE) {
  if (null_ok && is.null(x)) {
    return(TRUE)
  }
  if (length(x) != 1L) {
    return(FALSE)
  }
  x <- as.character(x)
  grepl("^\\d{4}(M(0[1-9]|1[0-2])|Q[1-4])?$", x)
}
