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

#### Obinna (OBI) Obianom

#### 2026-08-26

## 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

- `vignette("quickcode_r_introduction")` for a broader tour of the package
- `vignette("nullish_coalescing_operator_r")` for the closely related `%or%` and `%eo%` operators, which supply a fallback value instead of just testing for one