When dealing with user input using packages shiny or plumber one often needs to convert character arguments to numeric or logical.
I would like to do it automatically, what's an efficient way to do it ?
expected (this or similar) :
convert_args <- ...
fun <- function(a, b, c, d){
convert_args()
dplyr::lst(a, b, c , d)
}
fun("na","true","1","foo")
#> $a
#> [1] NA
#>
#> $b
#> [1] TRUE
#>
#> $c
#> [1] 1
#>
#> $d
#> [1] "foo"
One option is to use readr::parse_guess which as the name suggests tries to guess the type of the character vector.
convert_args <- function(x) {
lapply(x, readr::parse_guess)
}
convert_args(c("NA","true","1","foo"))
#[[1]]
#[1] NA
#[[2]]
#[1] TRUE
#[[3]]
#[1] 1
#[[4]]
#[1] "foo"
This doesn't directly work when we have "na"
readr::parse_guess("na")
#[1] "na"
but as #Moody_Mudskipper mentions it can be resolved specifying na argument in parse_guess
readr::parse_guess("na", c("na", "NA"))
#[1] NA
I built a wrapper around readr::parse_guess thanks to #Ronak's solution to get exactly the expected output.
I also added an option to evaluate the unconverted character input as it's a common task as well.
convert_args <- function(na = c("", "NA"), locale = readr::default_locale(),
trim_ws = TRUE, guess_integer = FALSE, eval = FALSE){
if(!requireNamespace("readr"))
stop("convert_args() requires package readr to be installed")
args <- as.list(eval.parent(quote(match.call())))[-1]
args <- lapply(args, readr::parse_guess, na, locale, trim_ws, guess_integer)
if (eval){
args <- lapply(args, function(arg) {
if(is.character(arg))
eval(parse(text = arg, parent.frame(2)))
else
arg
})
}
list2env(args, envir = parent.frame())
invisible(NULL)
}
fun <- function(a, b, c, d){
convert_args()
dplyr::lst(a, b, c , d)
}
fun("NA","true","1","head(cars,2)")
#> Loading required namespace: readr
#> $a
#> [1] NA
#>
#> $b
#> [1] TRUE
#>
#> $c
#> [1] 1
#>
#> $d
#> [1] "head(cars,2)"
fun2 <- function(a, b, c, d){
convert_args(eval = TRUE, na = c("na","NA"))
dplyr::lst(a, b, c , d)
}
fun2("na","true","1","head(cars,2)")
#> $a
#> [1] NA
#>
#> $b
#> [1] TRUE
#>
#> $c
#> [1] 1
#>
#> $d
#> speed dist
#> 1 4 2
#> 2 4 10
Created on 2019-06-21 by the reprex package (v0.3.0)
Related
I am trying to assemble a list out of a vector and a row of a data
frame. The list will be passed to do.call() as the arguments to a
function. If the vector is length 1, no problem.
tbl <- tibble::tibble(a = 1:4,
b = letters[1:4])
vec <- 1
works <- c(avec = vec, as.list(tbl[1,]))
testit <- function(avec, a, b){
length(avec) + length(a) + length(b)
}
do.call(testit, works)
#> [1] 3
But it also needs to work with longer vectors
vec <- 1:2
broken <- c(avec = vec, as.list(tbl[2,]))# breaks apart avec
do.call(testit, broken)
#> Error in (function (avec, a, b) : unused arguments (avec1 = 1, avec2 = 2)
toomany <- list(avec = vec, as.list(tbl[2,]))#too many layers
do.call(testit, toomany)
#> Error in (function (avec, a, b) : argument "b" is missing, with no default
#what I want:
whatIwant <- list(avec = 1:2, a = 2, b = "b")
do.call(testit, whatIwant)
#> [1] 4
It doesn’t matter if a data frame, and I want solution to work with both
tibbles and dataframes anyhow.
adf <- data.frame(a = 1:4,
b = letters[1:4], stringsAsFactors = FALSE)
list(avec = vec, as.list(adf[1,]))
#> $avec
#> [1] 1 2
#>
#> [[2]]
#> [[2]]$a
#> [1] 1
#>
#> [[2]]$b
#> [1] "a"
Other things I’ve tried.
purrr::flatten(toomany) # breaks up avec again
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 2
#>
#> $a
#> [1] 2
#>
#> $b
#> [1] "b"
c(avec = vec, as.list(adf[1,]), recursive = TRUE)
#> avec1 avec2 a b
#> "1" "2" "1" "a"
list(avec = vec, as.vector(adf[1,]))
#> $avec
#> [1] 1 2
#>
#> [[2]]
#> a b
#> 1 1 a
list(vec, unlist(adf[1,]))
#> [[1]]
#> [1] 1 2
#>
#> [[2]]
#> a b
#> "1" "a"
I didn’t think this would be so hard! Do I have to assemble the list in
text and parse it? I’m missing something. Created on 2019-03-01 by the
reprex package (v0.2.0).
Consider
l1 <- list("a", NA, 1:3, NA)
l2 <- list("a", NULL, 1:3, NULL)
Why does Filter(Negate(is.na), l1) not work, while Filter(Negate(is.null), l2) does work as expected (it returns all elements of l2 which are not NULL)?
The helpfile for ?Filter says
...Note that possible NA values are currently always taken as false; control over NA handling may be added in the future.
I still dont really understand the behaviour.
is.na returns a vector for each element of the list; you want anyNA (or perhaps exactlyNA as defined below):
l1 <- list("a", NA, 1:3, NA)
l2 <- list("a", NULL, 1:3, NULL)
Filter(Negate(is.na), l1)
#> [[1]]
#> [1] "a"
#>
#> [[2]]
#> [1] 1 2 3
#>
#> [[3]]
#> [1] NA
#>
#> [[4]]
#> NULL
Filter(Negate(anyNA), l1)
#> [[1]]
#> [1] "a"
#>
#> [[2]]
#> [1] 1 2 3
exactlyNA <- function(x) identical(x, NA)
Filter(Negate(exactlyNA), l1)
#> [[1]]
#> [1] "a"
#>
#> [[2]]
#> [1] 1 2 3
Created on 2018-11-13 by the reprex package (v0.2.1)
Your first example effectively tries to select the 1st, 3rd, 4th, and 5th elements of your list. Nothing to do with NA.
How do I remove an element from a list in R?
Imagine this workflow:
# create list
my_list <- lapply(1:10, function(x) x)
# find which ones to exclude
my_list_boolean <- sapply(my_list, function(x) ifelse(x%%2>0,F,T))
# does not work like this!
my_list[[my_list_boolean]]
Is there a solution not having to use a for loop and create a big logic around my statement?
Just use [] and not [[]]
my_list <- lapply(1:10, function(x) x)
# find which ones to exclude
my_list_boolean <- sapply(my_list, function(x) ifelse(x%%2>0,F,T))
# does not work like this!
my_list[my_list_boolean]
#> [[1]]
#> [1] 2
#>
#> [[2]]
#> [1] 4
#>
#> [[3]]
#> [1] 6
#>
#> [[4]]
#> [1] 8
#>
#> [[5]]
#> [1] 10
Created on 2018-11-03 by the reprex package (v0.2.1)
You can thus select element of the list with logical vector and not the content (which is [[]]
Do you mean this?
my_list[my_list_boolean]
#[[1]]
#[1] 2
#
#[[2]]
#[1] 4
#
#[[3]]
#[1] 6
#
#[[4]]
#[1] 8
#
#[[5]]
#[1] 10
I have a problem with R's grep() function apparently finding an "l" everywhere:
> l <- list(list(), list("a"), list("a","l"))
> grep("a",l)
[1] 2 3
> grep("l",l)
[1] 1 2 3
> grep("l",l,fixed=TRUE)
[1] 1 2 3
This problem seems to occur only with the letter "l". Does anyone have a hint on that?
Many thanks,
Cord
If you look at the documentation for the argument x in grep you'll see that it should be
a character vector where matches are sought, or an object which can be coerced by as.character to a character vector.
If you try that operation you'll see what goes wrong:
> as.character(l)
[1] "list()" "list(\"a\")" "list(\"a\", \"l\")"
so the same "problem" happens if you grep for i, s etc.
You could try the following instead
sapply(l, function(i) grep("l", i))
which produces
[[1]]
integer(0)
[[2]]
integer(0)
[[3]]
[1] 2
Interesting post, I never knew grep convert the x vector like this:
l <- list(list(), list("a"), list("a","l"))
l
#> [[1]]
#> list()
#>
#> [[2]]
#> [[2]][[1]]
#> [1] "a"
#>
#>
#> [[3]]
#> [[3]][[1]]
#> [1] "a"
#>
#> [[3]][[2]]
#> [1] "l"
Internally grep is converting l to a character vector
grep
#> function (pattern, x, ignore.case = FALSE, perl = FALSE, value = FALSE,
#> fixed = FALSE, useBytes = FALSE, invert = FALSE)
#> {
#> if (!is.character(x))
#> x <- structure(as.character(x), names = names(x))
#> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#> .Internal(grep(as.character(pattern), x, ignore.case, value,
#> perl, fixed, useBytes, invert))
#> }
#> <bytecode: 0x0000000012e18610>
#> <environment: namespace:base>
So now l is actually:
structure(as.character(l), names = names(l))
#> [1] "list()" "list(\"a\")" "list(\"a\", \"l\")"
Which has "l" in each.
You could unlist l first to get expected results:
ul <- unlist(l)
ul
#> [1] "a" "a" "l"
grep("a",l)
#> [1] 2 3
grep("a",ul)
#> [1] 1 2
grep("l",l)
#> [1] 1 2 3
grep("l",ul)
#> [1] 3
grep("l",l,fixed=TRUE)
#> [1] 1 2 3
grep("l",ul,fixed=TRUE)
#> [1] 3
I have a nested list containing NULL elements, and I'd like to replace those with something else. For example:
l <- list(
NULL,
1,
list(
2,
NULL,
list(
3,
NULL
)
)
)
I want to replace the NULL elements with NA. The natural way to do this is to recursively loop over the list using rapply. I tried:
rapply(l, function(x) NA, classes = "NULL", how = "replace")
rapply(l, function(x) if(is.null(x)) NA else x, how = "replace")
Unfortunately, neither of these methods work, since rapply apparently ignores NULL elements.
How can I manipulate the NULL elements in a nested list?
I'm going to go with "use a version of rapply doesn't doesn't have weird behaviour with NULL". This is the simplest implementation I can think of:
simple_rapply <- function(x, fn)
{
if(is.list(x))
{
lapply(x, simple_rapply, fn)
} else
{
fn(x)
}
}
(rawr::rapply2, as mentioned in the comments by #rawr is a more sophisticated attempt.)
Now I can do the replacement using
simple_rapply(l, function(x) if(is.null(x)) NA else x)
This is what William Dunlap suggested in 2010 when this question was asked on Rhelp:
replaceInList <- function (x, FUN, ...)
{
if (is.list(x)) {
for (i in seq_along(x)) {
x[i] <- list(replaceInList(x[[i]], FUN, ...))
}
x
}
else FUN(x, ...)
}
replaceInList(l, function(x)if(is.null(x))NA else x)
This is a hack, but as far as hacks go, I think I'm somewhat happy with it.
lna <- eval(parse(text = gsub("NULL", "NA", deparse(l))))
str(lna)
#> List of 3
#> $ : logi NA
#> $ : num 1
#> $ :List of 3
#> ..$ : num 2
#> ..$ : logi NA
#> ..$ :List of 2
#> .. ..$ : num 3
#> .. ..$ : logi NA
Update:
If for some reason you needed "NULL" as a character entry in the list (corner case, much?) you can still use the above hack since it replaces the contents of the string, not the quotes, thus it just requires another step
l2 <- list(
NULL,
1,
list(
2,
"NULL",
list(
3,
NULL
)
)
)
lna2 <- eval(parse(text = gsub("NULL", "NA", deparse(l2))))
lna2_2 <- eval(parse(text = gsub('\\"NA\\"', '\"NULL\"', deparse(lna2))))
str(lna2_2)
#> List of 3
#> $ : logi NA
#> $ : num 1
#> $ :List of 3
#> ..$ : num 2
#> ..$ : chr "NULL"
#> ..$ :List of 2
#> .. ..$ : num 3
#> .. ..$ : logi NA
I wrapped the replacement inside the sapply, which makes it more readable/understandable to me, albeit less general.
replace_null <- function(x) {
lapply(x, function(x) {
if (is.list(x)){
replace_null(x)
} else{
if(is.null(x)) NA else(x)
}
})
}
replace_null(l)
This can also be done with rrapply() in the rrapply-package. Below are a few different ways we could replace the NULL elements in a nested list by NA values:
library(rrapply)
l <- list(
NULL,
1,
list(
2,
NULL,
list(
3,
NULL
)
)
)
## replace NULL by NA using only f
rrapply(l, f = function(x) if(is.null(x)) NA else x, how = "replace")
#> [[1]]
#> [1] NA
#>
#> [[2]]
#> [1] 1
#>
#> [[3]]
#> [[3]][[1]]
#> [1] 2
#>
#> [[3]][[2]]
#> [1] NA
#>
#> [[3]][[3]]
#> [[3]][[3]][[1]]
#> [1] 3
#>
#> [[3]][[3]][[2]]
#> [1] NA
## replace NULL by NA using condition argument
rrapply(l, condition = is.null, f = function(x) NA, how = "replace")
#> [[1]]
#> [1] NA
#>
#> [[2]]
#> [1] 1
#>
#> [[3]]
#> [[3]][[1]]
#> [1] 2
#>
#> [[3]][[2]]
#> [1] NA
#>
#> [[3]][[3]]
#> [[3]][[3]][[1]]
#> [1] 3
#>
#> [[3]][[3]][[2]]
#> [1] NA
## replace NULL by NA using condition and deflt arguments
rrapply(l, condition = Negate(is.null), deflt = NA, how = "list")
#> [[1]]
#> [1] NA
#>
#> [[2]]
#> [1] 1
#>
#> [[3]]
#> [[3]][[1]]
#> [1] 2
#>
#> [[3]][[2]]
#> [1] NA
#>
#> [[3]][[3]]
#> [[3]][[3]][[1]]
#> [1] 3
#>
#> [[3]][[3]][[2]]
#> [1] NA
We can also prune the NULL elements from the list altogether by setting how = "prune":
## keep only non-NULL elements
rrapply(l, condition = Negate(is.null), how = "prune")
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [[2]][[1]]
#> [1] 2
#>
#> [[2]][[2]]
#> [[2]][[2]][[1]]
#> [1] 3