A comprehensive guide to the NOT functions for data validation in quickcode

Obinna (OBI) Obianom

2026-08-03

Why NOT functions

Base R gives you plenty of is.*() checks — is.null(), is.na(), is.numeric(), is.vector() and so on — but it does not give you their opposites. In practice, negated checks are extremely common: “is this argument not missing”, “is this object not a data frame”, “is this value not already in my lookup vector”. Without a dedicated function, every one of these checks ends up written as !is.null(x), !is.na(x), !(x %in% table), scattered across a script with a leading ! that is easy to miss when skimming code, and easy to accidentally drop or double up when editing.

The quickcode package’s NOT functions exist to make these checks first-class, readable, and safe to compose. not.null(x) reads exactly like the English sentence it represents, so validation code and guard clauses stop requiring a mental “flip the meaning” step every time a reader hits a !.

This vignette walks through the full family of NOT functions, what each one actually checks, and some patterns for using them together to validate function inputs and clean up data.

The full NOT function family


not.null          Opposite of is.null() - check if entry is NOT NULL
not.na            Opposite of is.na() - check if entry is NOT NA
not.empty         Check if entry is NOT empty (NULL, NA, or "")
is.empty          Check if entry IS empty (companion to not.empty)
not.numeric       Opposite of is.numeric()
not.integer       Opposite of is.integer()
not.logical       Opposite of is.logical()
not.vector        Opposite of is.vector()
not.data          Opposite of is.data.frame()
not.environment   Opposite of is.environment()
not.Date          Opposite of inherits(x, "Date")
not.duplicated    Opposite of duplicated()
not.image         Opposite of is.image() - file extension is NOT an image type
not.inherits      Opposite of inherits() - object does NOT inherit from given class(es)
not.exists        Opposite of exists() - object does NOT exist in scope
has.error         Whether an expression or function call produces an error
%nin%             Opposite of %in% - value is NOT in a vector/table (alias %!in%)

Every function above returns a logical value (or logical vector), so they drop directly into if(), while(), stopifnot(), Filter(), or logical indexing, exactly like their is.*() counterparts.

Core object checks

not.null


is.null("")          # FALSE
not.null("")         # TRUE
not.null(NULL)        # FALSE

if (not.null(45)) message("something") # prints "something"

not.null() is the one you will reach for constantly, especially at the top of a function to make sure a required argument was actually supplied.

not.na


not.na(NA)                     # FALSE
not.na(NULL)                    # logical(0), since is.na(NULL) is also logical(0)

if (not.na(45)) message("something") # prints "something"

Note that, just like is.na(), not.na() is vectorized: passing a vector returns a logical vector of the same length, one result per element.

not.empty / is.empty


not.empty("empty")     # TRUE
not.empty('')           # FALSE
not.empty(y <- NULL)    # FALSE

is.empty("")            # TRUE
is.empty("empty")       # FALSE

if (not.empty('')) message("yes") # nothing printed, condition is FALSE

not.empty() combines three checks in one call: it is FALSE whenever the value is NULL, is NA, or is the empty string "". This is convenient for validating text inputs, such as form fields or CLI arguments, where any of those three “nothing was provided” states should be treated the same way. is.empty() is simply the inverse, provided so both directions read naturally depending on context.

Type and class checks

not.numeric, not.integer, not.logical, not.vector, not.data, not.environment


not.numeric("45")   # TRUE  (a character string is not numeric)
not.numeric(45)      # FALSE

is.integer(78L)      # TRUE
not.integer(78L)     # FALSE
not.integer(23.43)   # TRUE, since 23.43 is a double, not an integer

test.env <- TRUE
test.notenv <- 0
not.logical(test.env)     # FALSE
not.logical(test.notenv)  # TRUE

vect1 <- list(r = 1, t = 3:10)
vect2 <- LETTERS
not.vector(vect1) # FALSE, a list is still a vector in R's type system
not.vector(vect2) # FALSE

test.dt <- data.frame(ID = 1:200, Type = "RPKG.net")
test.notdf <- list(t = 1)
not.data(test.dt)    # FALSE
not.data(test.notdf) # TRUE

test.env2 <- new.env()
test.notenv2 <- list(t = 1)
not.environment(test.env2)     # FALSE
not.environment(test.notenv2)  # TRUE

These map one-to-one onto their base R is.*() counterparts, so they carry the exact same semantics — not.integer(45) is TRUE for the same reason is.integer(45) is FALSE: an unsuffixed numeric literal in R is a double, not an integer, unless written as 45L.

not.Date


d1 <- as.Date("2024-01-01")
d2 <- "2024-01-01"

is.Date(d1)   # TRUE
not.Date(d1)  # FALSE
not.Date(d2)  # TRUE, a plain character string is not a Date object

not.inherits


keep.cols <- "a character"
class(keep.cols) # "character"
not.inherits(keep.cols, "character") # FALSE

num.var <- 1L
class(num.var) # "integer"
not.inherits(num.var, "double") # TRUE

not.inherits() is the negated form of base R’s inherits(), and accepts the same what (a character vector of class names) and which arguments, so you can check against several possible classes at once.

Working with collections

not.duplicated


set.seed(08082023)
dtf <- sample(1:10, 15, replace = TRUE)
dtf # 3  9 10  3  8  9  6 10  5  1  2  2  2  9  8

dtf[dtf > 4 & duplicated(dtf)]     # values that ARE duplicates: 9 10  9  8
dtf[dtf > 4 & not.duplicated(dtf)] # values that are NOT duplicates: 9 10  8  6  5

Because not.duplicated() mirrors duplicated() argument-for-argument (including incomparables), it slots directly into logical indexing anywhere you’d normally combine !duplicated(x).

%nin% (not in)


5 %nin% c(1:10)   # FALSE, 5 is in 1:10
5 %nin% c(11:20)  # TRUE, 5 is not in 11:20

x <- "a"
if (x %nin% letters) x # condition is FALSE, so nothing runs

# exclude specific values from a vector
vector_num1 <- number(9, max.digits = 5, seed = 1)
vector_num1[vector_num1 %nin% c(83615, 85229)] # keep everything except those two values

%nin% also has the alias %!in%, for readers who prefer an operator that visually echoes %in% with a ! in front of it.

File and error checks

not.image / is.image


img.1 <- "fjk.jpg"
not.image(img.1) # FALSE

img.2 <- "fjk.bmp"
not.image(img.2) # FALSE

img.3 <- "fjk.SVG" # extension matching is case-insensitive
not.image(img.3)   # FALSE

v <- c("logo.png", "business process.pdf",
       "front_cover.jpg", "intro.docx",
       "financial_future.doc", "2022 buybacks.xlsx")
not.image(v) # FALSE TRUE FALSE TRUE TRUE TRUE

# when a file name has no extension, both is.image() and not.image()
# return NA rather than guessing
v2 <- c("img2.jpg", NA, "northbound.xlsx", "landimg")
not.image(v2) # FALSE NA TRUE NA

is.image()/not.image() recognize a broad list of common raster and vector image formats (PNG, JPG/JPEG, GIF, BMP, SVG, TIFF, HEIC/HEIF, WebP, PSD, AI, and several camera raw formats among others), so both functions are handy for filtering a directory listing down to (or away from) image files without hand-rolling a regular expression.

has.error


# this should not produce an error, so the result is FALSE
has.error({
  x <- 8
  y <- number(10)
  res <- x + y
})

# this produces an error ("non-numeric argument to binary operator"),
# so the result is TRUE - but the error itself is caught, not thrown
has.error({
  x <- 8
  y <- "random"
  res <- x + y
})

# accessing a column that doesn't exist also errors, so this is TRUE
df1 <- mtcars
has.error(df1[, "rpkg.net"])

has.error() wraps its argument in tryCatch() and reports whether the expression raised an error, without letting that error propagate and stop your script. This makes it useful for validating that user-supplied code, formulas, or file paths behave as expected before you rely on them further downstream, without wrapping every call site in its own tryCatch() block.

Putting it together: a validation guard clause

A common pattern is to combine several NOT functions at the top of a function to validate its inputs before doing any real work:


process_dataset <- function(df, id_col) {

  if (not.data(df)) stop("df must be a data.frame")
  if (is.empty(id_col)) stop("id_col must not be empty")
  if (id_col %nin% names(df)) stop("id_col must be one of the column names in df")
  if (not.numeric(df[[id_col]])) stop("the id_col column must be numeric")

  # ... continue processing, confident the inputs are valid
  df
}

Because every NOT function returns a plain logical value, they compose with &&, ||, all(), and any() exactly like base R’s own is.*() functions — the only difference is that your validation code reads the way you’d say it out loud.

See also