Skip to content

Latest commit

 

History

History
357 lines (303 loc) · 10.8 KB

inlist.src.org

File metadata and controls

357 lines (303 loc) · 10.8 KB

inlist

#+r-pkg-version: 0.0.0.9000 #+r-pkg-url: https://github.com/stasvlasov/inlist #+r-pkg-bug-reports: https://github.com/stasvlasov/inlist/issues

Description

:export_options+: author:nil :export_options+: title:nil

#+end

Its main function inlist(your_list, i, j) filters your_list by i and applies j. For those familiar with data.table the inlist’s interface is similar to data.table’s `[` extractor method (i.e., data.table:::`[.data.table`(dt, i ,j) means subset dt using i and manipulate with j). Each list’s element is bound to environment where filtering (i) and selection (j) is evaluated However, unlike data.table’s i and j list’s named elements in inlist are bound to variables that are prefixed with .. It also binds special variables ., .., ..., .n, .N, ..n and ..N variables for accessing the list element itself, the original list, elements’ index, length of the list, etc. as well as a special function ._() which can be used to subsitute default values when some list’s elemenst are not available.

Usage

l <- list(list(a = 1
             , b = 1)
        , list(a = 2
             , b = 2
             , c = 2)
        , list(a = 3
             , b = 3
             , e = 3)
        , list(a = 4
             , b = 4
             , e = 4
             , d = 4
             , f = 4))


inlist(l, .n == length(.), .b)

# [[1]]
# [1] 3

inlist(l, .e, .a)

# [[1]]
# [1] 3
# 
# [[2]]
# [1] 4

inlist(l, , .a)

# [[1]]
# [1] 1
# 
# [[2]]
# [1] 2
# 
# [[3]]
# [1] 3
# 
# [[4]]
# [1] 4

inlist(l, , paste("Hello", .a, "world!"))

# [[1]]
# [1] "Hello 1 world!"
# 
# [[2]]
# [1] "Hello 2 world!"
# 
# [[3]]
# [1] "Hello 3 world!"
# 
# [[4]]
# [1] "Hello 4 world!"

inlist(l, , paste("Hello", .e, "world!"))

# [[1]]
# [1] "Hello 3 world!"
# 
# [[2]]
# [1] "Hello 4 world!"

inlist(l, , paste("Hello", .a, ._(.e + ._(.f, 10) , "brave"), "world!"))

# [[1]]
# [1] "Hello 1 brave world!"
# 
# [[2]]
# [1] "Hello 2 brave world!"
# 
# [[3]]
# [1] "Hello 3 13 world!"
# 
# [[4]]
# [1] "Hello 4 8 world!"



Installation

devtools::install_github("<<github-repo()>>")

Dependencies

nameversion
utils
nameversioncomment
tinytestfor package development (unit testing)

R Code

##' Select and apply expression to elements of table like lists.
##'
##' Each list element is attached to both `extractor` and `applicator` environment so its own named elements are available as variables prefixed with "." (dot character).
##'
##' - `.` - current list's element
##' - `..` - (filtered) list (in `extractor` environment it is the same as initial list)
##' - `.n` - (filtered) list element's index
##' - `.N` - (filtered) list's length
##' - `...` - initial list (`l`) 
##' - `..n` - initial list element's index
##' - `..N` - initial list's length
##' - `._(x, fb)` - function that returns the fallback `fb` if some of the variables in `x` are unbound (i.e., due to non existing list elements)
##' 
##' @param l list (or NULL)
##' @param extractor Predicate expression to filter list elements before applying evaluated in the environment of list's element
##' @param applicator Expression applied to each element of the list evaluated in the element's environment
##' @param fallback Optional. If provided use this value as a fall back in case some variables (prefixed with dot) are not fount in the list's element environment. Otherwise (the default) those elements will be ignored and not included to results
##' 
##' @return List of filtered with `extractor` elements with values returned by `applicator`. Unbound expressions are omitted (if `fallback` is not provided) so list might be shorter.
##' 
##' @export 
inlist <- function(l, extractor, applicator, fallback) {
    if(!is.list(l) & !is.null(l)) stop("inlist -- argument `l` should be either list or NULL")
    if(base::missing(fallback)) {
        fallback_void <- TRUE
        fallback <- NULL
    } else {
        fallback_void <- FALSE
    }
    sys_call <- as.list(sys.call())
    parent_frame <- parent.frame()
    ## eval in elements envir
    .eval <- function(envir, index, x, fallback, wrapper, .l, n = index, prefix_dots = TRUE) {
        ## `(` is identity function
        ## prepend dot to names
        if(length(envir) > 0 & prefix_dots) {
            names(envir) <- ifelse(names(envir) != "", paste0(".", names(envir)), "")
        }
        envir <- c(envir, list(. = envir
                             , .. = .l
                             , .n = index
                             , .N = length(.l)
                             , ... = l
                             , ..n = n
                             , ..N = length(l)
                             , ._ = \(x, fallback = NULL) {
                                 .eval(envir
                                     , index
                                     , as.list(sys.call())[[2]] # i.e., x
                                     , fallback
                                     , `(`
                                     , .l
                                     , n
                                     , prefix_dots = FALSE)
                             }))
        vars_skip <- NULL
        ## check if function was used and eval even if args are not bound
        if("._" %in% all.names(x)) {
            x_data <-
                parse(text = deparse1(x), keep.source = TRUE) |>
                utils::getParseData()
            while(!is.na(i <- which(x_data$text == "._")[1])) {
                x_data <- x_data[-(1:i),]
                x_data <- x_data[x_data$token != "expr",]
                x_data <- split(x_data, cumsum(x_data$parent == x_data$parent[1]))
                ## if there are parent elements then there were arguments
                if(length(x_data) %in% c(2,3)) {
                    ._first_arg <- x_data[[1]][-1,]
                    vars_skip <- c(vars_skip, ._first_arg[._first_arg$token == "SYMBOL", "text"])
                    x_data <- x_data[[length(x_data)]][-1,]
                } else {
                    stop("inlist -- wrong number of arguments in ._() function")
                }
            }
        }
        vars <- all.vars(x)
        ## remove vars that are in ._
        vars <- vars[!(vars %in% vars_skip)]
        ## find vars names that starts with .
        vars <- vars[substr(vars,0,1) == "."]
        vars_exist <- sapply(vars, \(v) eval(bquote(exists(.(v))), envir, parent_frame))
        if(all(vars_exist)) {
            do.call(wrapper, list(eval(x, envir, parent_frame)))
        } else {
            return(fallback)
        }
    }
    ## apply with index
    .apply <- function(.l, x, wrapper = `(`, fallback, ..n = NULL) {
        mapply(
            \(e, i, n) .eval(e, i, x, fallback, wrapper, .l, n)
          , e = .l
          , i = seq_along(.l)
          , n = if(is.null(..n)) seq_along(.l) else ..n
          , SIMPLIFY = FALSE)
    }
    ## filter
    if(missing(extractor)) {
        extract_l <- TRUE
    } else {
        extractor <- sys_call[[3]]
        extract_l <- .apply(l, extractor, fallback = FALSE, wrapper = Negate(isFALSE)) |> unlist()
    }
    ## map
    if(missing(applicator)) {
        apply_l <- l[extract_l]
    } else {
        applicator <- sys_call[[4]]
        apply_l <- .apply(l[extract_l], applicator, fallback, wrapper = list, ..n = seq_along(l)[extract_l])
    }
    ## return()
    if(fallback_void) {
        apply_l <- Filter(Negate(is.null), apply_l)
    }
    return(lapply(apply_l, unlist, recursive = FALSE))
}
l <- list(list(a = 1
             , b = 1)
        , list(a = 2
             , b = 2
             , c = 2)
        , list(a = 3
             , b = 3
             , e = 3)
        , list(a = 4
             , b = 4
             , e = 4
             , d = 4
             , f = 4))

## test placeholders
expect_equal(
    inlist(l, .n == length(.), .b)
  , list(3)
)

## test filter
expect_equal(
    inlist(l, .e, .a)
  , list(3, 4)
)


expect_equal(
    inlist(l, , .a)
  , list(1, 2, 3, 4)
)


expect_equal(
    inlist(l, , paste("Hello", .a, "world!"))
  , list("Hello 1 world!", "Hello 2 world!", "Hello 3 world!", "Hello 4 world!")
)


expect_equal(
    inlist(l, , paste("Hello", .e, "world!"))
  , list("Hello 3 world!", "Hello 4 world!")
)



## substitutes
expect_equal(
    inlist(l, , ._(paste("f is", .f), "f does not exist"))
  , list("f does not exist"
       , "f does not exist"
       , "f does not exist"
       , "f is 4")
)



expect_equal(
    inlist(l, , paste("Hello", .a, ._(.e + ._(.f, 10) , "brave"), "world!"))
  , list("Hello 1 brave world!"
       , "Hello 2 brave world!"
       , "Hello 3 13 world!"
       , "Hello 4 8 world!"))


## Check types
l <- NULL
expect_equal(
    inlist(l, .n == length(.), .b)
  , list()
)


l <- list()
expect_equal(
    inlist(l, .n == length(.), .b)
  , list()
)


l <- ""
expect_error(
    inlist(l, .n == length(.), .b)
)

l <- 1
expect_error(
    inlist(l, .n == length(.), .b)
)

l <- NA
expect_error(
    inlist(l, .n == length(.), .b)
)