rrlangnse

Evaluating naked expressions case-insensitively


I'd like to create a function that checks if naked expressions are contained in a vector or not. That function should work independently of the case of the input data.

A simple solution to evaluate a variety of expressions is to use tidyselect::eval_select(). E.g. here's a dummy function that checks whether or not expressions are a, b or c.

is_abc <- function(...) {
  expr <- rlang::expr(c(...))
  data <- rlang::set_names(letters)
  out <- tidyselect::eval_select(expr, data)
  names(out) %in% letters[1:3]
}

is_abc(a, b, z)
#> [1]  TRUE  TRUE FALSE

is_abc(c(a, b, z))
#> [1]  TRUE  TRUE FALSE

Created on 2023-12-27 by the reprex package (v2.0.1)

The problem is that I don't think it's possible to make it case-insensitive without editing tidyselect::eval_select() (or using a special selection helper).

is_abc(c(a, B, z))
#> Error in `is_abc()`:
#> ! Can't subset columns that don't exist.
#> ✖ Column `B` doesn't exist.

Alternatively, I could use rlang::enexprs() to get a list of symbols that will then be converted to character type when changing the case or comparing to the vector:

is_abc <- function(...) {
  exprs <- rlang::enexprs(...)
  tolower(exprs) %in% tolower(letters[1:3])
}

is_abc(a, b, z)
#> [1]  TRUE  TRUE FALSE

is_abc(a, B, z)
#> [1]  TRUE  TRUE FALSE

Created on 2023-12-27 by the reprex package (v2.0.1)

The problem now is that this won't work for more complex expressions since that will convert entire expressions to strings:

is_abc(c(a, b, z))
#> [1] FALSE

Is there a simple solution to this problem so that I can have a function working in both cases, without depending on the case of the input?

For example:

# desired behaviour

is_abc(c(a, B), z)
#> [1]  TRUE  TRUE FALSE

Solution

  • You could do the whole thing in base R:

    is_abc <- function(...) {
      
      args <- substitute(...())
      recurse <- FALSE
      if(".recurse" %in% names(args)) {
        args <- args[-match(".recurse", names(args))]
        recurse <- TRUE
      }
      out <- unlist(lapply(args, function(x) {
        if(is.call(x)) {
          x <- as.list(x)
          if(!identical(x[[1]], quote(c))) {
            stop("is_abc does not know what to do with calls to function '", 
                 as.character(x[[1]]), "'")
          }
          unlist(do.call("is_abc", c(x[-1], .recurse = 1)))
        } else as.character(x)
        }))
      
      if(recurse) toupper(out) else toupper(out) %in% c("A", "B", "C")
    }
    

    Testing:

    is_abc(a, b, z)
    #> [1]  TRUE  TRUE FALSE
    
    is_abc(A, b, z)
    #> [1]  TRUE  TRUE FALSE
    
    is_abc(c(a, b, z))
    #> [1]  TRUE  TRUE FALSE
    
    is_abc(c(a, B, z))
    #> [1]  TRUE  TRUE FALSE
    
    is_abc(c(a, B), z)
    #> [1]  TRUE  TRUE FALSE
    
    is_abc(a, b, c(z, c, c(a, b, c(z, y, c(a, b, c)))))
    #> [1]  TRUE  TRUE FALSE  TRUE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE