diff --git a/NEWS.md b/NEWS.md index 5d5177af..a9a63ef2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # purrr (development version) +* Added a new keep_empty argument to `list_c()`, `list_cbind()`, and `list_rbind()` which will keep empty elements as `NA` in the returned data frame (@SokolovAnatoliy, #1096). * `list_transpose()` now works with data.frames (@KimLopezGuell, #1109). * Added `imap_vec()` (#1084) * `list_transpose()` inspects all elements to determine the correct diff --git a/R/list-combine.R b/R/list-combine.R index ea31d5de..f9c8abf6 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -22,6 +22,9 @@ #' same size (i.e. number of rows). #' @param name_repair One of `"unique"`, `"universal"`, or `"check_unique"`. #' See [vctrs::vec_as_names()] for the meaning of these options. +#' @param keep_empty An optional logical. If `FALSE` (the default), then +#' empty (`NULL`) elements are silently ignored; if `TRUE`, then empty +#' elements are preserved by converting to `NA`. #' @inheritParams rlang::args_dots_empty #' @export #' @examples @@ -30,16 +33,21 @@ #' #' x2 <- list( #' a = data.frame(x = 1:2), -#' b = data.frame(y = "a") +#' b = data.frame(y = "a"), +#' c = NULL #' ) #' list_rbind(x2) #' list_rbind(x2, names_to = "id") +#' list_rbind(x2, names_to = "id", keep_empty = TRUE) #' list_rbind(unname(x2), names_to = "id") -#' #' list_cbind(x2) -list_c <- function(x, ..., ptype = NULL) { +#' list_cbind(x2, keep_empty = TRUE) +list_c <- function(x, ..., ptype = NULL, keep_empty = FALSE) { vec_check_list(x) check_dots_empty() + if (keep_empty) { + x <- convert_null_to_NA(x) + } # For `list_c()`, we don't expose `list_unchop()`'s `name_spec` arg, # and instead strip outer names to avoid collisions with inner names @@ -58,19 +66,26 @@ list_cbind <- function( x, ..., name_repair = c("unique", "universal", "check_unique"), - size = NULL + size = NULL, + keep_empty = FALSE ) { check_list_of_data_frames(x) check_dots_empty() + if (keep_empty) { + x <- convert_null_to_NA(x) + } vec_cbind(!!!x, .name_repair = name_repair, .size = size, .error_call = current_env()) } #' @export #' @rdname list_c -list_rbind <- function(x, ..., names_to = rlang::zap(), ptype = NULL) { +list_rbind <- function(x, ..., names_to = rlang::zap(), ptype = NULL, keep_empty = FALSE) { check_list_of_data_frames(x) check_dots_empty() + if (keep_empty) { + x <- convert_null_to_NA(x) + } vec_rbind(!!!x, .names_to = names_to, .ptype = ptype, .error_call = current_env()) } @@ -95,3 +110,9 @@ check_list_of_data_frames <- function(x, error_call = caller_env()) { call = error_call ) } + +convert_null_to_NA <- function(x) { + is_null <- map_lgl(x, is.null) + x[is_null] <- list(NA) + x +} diff --git a/man/list_c.Rd b/man/list_c.Rd index d4f6167c..d88f57ec 100644 --- a/man/list_c.Rd +++ b/man/list_c.Rd @@ -6,16 +6,17 @@ \alias{list_rbind} \title{Combine list elements into a single data structure} \usage{ -list_c(x, ..., ptype = NULL) +list_c(x, ..., ptype = NULL, keep_empty = FALSE) list_cbind( x, ..., name_repair = c("unique", "universal", "check_unique"), - size = NULL + size = NULL, + keep_empty = FALSE ) -list_rbind(x, ..., names_to = rlang::zap(), ptype = NULL) +list_rbind(x, ..., names_to = rlang::zap(), ptype = NULL, keep_empty = FALSE) } \arguments{ \item{x}{A list. For \code{list_rbind()} and \code{list_cbind()} the list must @@ -26,6 +27,10 @@ only contain only data frames or \code{NULL}.} \item{ptype}{An optional prototype to ensure that the output type is always the same.} +\item{keep_empty}{An optional logical. If \code{FALSE} (the default), then +empty (\code{NULL}) elements are silently ignored; if \code{TRUE}, then empty +elements are preserved by converting to \code{NA}.} + \item{name_repair}{One of \code{"unique"}, \code{"universal"}, or \code{"check_unique"}. See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for the meaning of these options.} @@ -53,11 +58,13 @@ list_c(x1) x2 <- list( a = data.frame(x = 1:2), - b = data.frame(y = "a") + b = data.frame(y = "a"), + c = NULL ) list_rbind(x2) list_rbind(x2, names_to = "id") +list_rbind(x2, names_to = "id", keep_empty = TRUE) list_rbind(unname(x2), names_to = "id") - list_cbind(x2) +list_cbind(x2, keep_empty = TRUE) } diff --git a/tests/testthat/_snaps/adverb-auto-browse.md b/tests/testthat/_snaps/adverb-auto-browse.md index 3ca159c5..18381b53 100644 --- a/tests/testthat/_snaps/adverb-auto-browse.md +++ b/tests/testthat/_snaps/adverb-auto-browse.md @@ -6,10 +6,3 @@ Error in `auto_browse()`: ! `.f` must not be a primitive function. ---- - - Code - auto_browse(identity)(NULL) - Output - NULL - diff --git a/tests/testthat/_snaps/deprec-prepend.md b/tests/testthat/_snaps/deprec-prepend.md index 5d47cc8b..3889f7b5 100644 --- a/tests/testthat/_snaps/deprec-prepend.md +++ b/tests/testthat/_snaps/deprec-prepend.md @@ -7,3 +7,27 @@ `prepend()` was deprecated in purrr 1.0.0. i Please use append(after = 0) instead. +# prepend throws error if before param is neither NULL nor between 1 and length(x) + + Code + prepend(list(), 1, before = 1) + Condition + Error in `prepend()`: + ! is.null(before) || (before > 0 && before <= n) is not TRUE + +--- + + Code + x %>% prepend(4, before = 0) + Condition + Error in `prepend()`: + ! is.null(before) || (before > 0 && before <= n) is not TRUE + +--- + + Code + x %>% prepend(4, before = 4) + Condition + Error in `prepend()`: + ! is.null(before) || (before > 0 && before <= n) is not TRUE + diff --git a/tests/testthat/_snaps/deprec-utils.md b/tests/testthat/_snaps/deprec-utils.md index 89c181b0..7bef1395 100644 --- a/tests/testthat/_snaps/deprec-utils.md +++ b/tests/testthat/_snaps/deprec-utils.md @@ -11,3 +11,35 @@ Warning: `rbernoulli()` was deprecated in purrr 1.0.0. +# rdunif fails if a and b are not unit length numbers + + Code + rdunif(1000, 1, "a") + Condition + Error in `rdunif()`: + ! is.numeric(a) is not TRUE + +--- + + Code + rdunif(1000, 1, c(0.5, 0.2)) + Condition + Error in `rdunif()`: + ! length(a) == 1 is not TRUE + +--- + + Code + rdunif(1000, FALSE, 2) + Condition + Error in `rdunif()`: + ! is.numeric(b) is not TRUE + +--- + + Code + rdunif(1000, c(2, 3), 2) + Condition + Error in `rdunif()`: + ! length(b) == 1 is not TRUE + diff --git a/tests/testthat/_snaps/deprec-when.md b/tests/testthat/_snaps/deprec-when.md index 483dee03..c888c011 100644 --- a/tests/testthat/_snaps/deprec-when.md +++ b/tests/testthat/_snaps/deprec-when.md @@ -7,3 +7,11 @@ `when()` was deprecated in purrr 1.0.0. i Please use `if` instead. +# error when named arguments have no matching conditions + + Code + 1:5 %>% when(a = sum(.) < 5 ~ 3) + Condition + Error in `when()`: + ! At least one matching condition is needed. + diff --git a/tests/testthat/_snaps/every-some-none.md b/tests/testthat/_snaps/every-some-none.md new file mode 100644 index 00000000..0b3bb46d --- /dev/null +++ b/tests/testthat/_snaps/every-some-none.md @@ -0,0 +1,16 @@ +# every() requires logical value + + Code + every(list(1:3), identity) + Condition + Error in `every()`: + ! `.p()` must return a single `TRUE` or `FALSE`, not an integer vector. + +--- + + Code + every(list(function() NULL), identity) + Condition + Error in `every()`: + ! `.p()` must return a single `TRUE` or `FALSE`, not a function. + diff --git a/tests/testthat/_snaps/map-depth.md b/tests/testthat/_snaps/map-depth.md index cc1a3ad4..b0bb90a7 100644 --- a/tests/testthat/_snaps/map-depth.md +++ b/tests/testthat/_snaps/map-depth.md @@ -20,6 +20,16 @@ Error in `map_depth()`: ! Negative `.depth` (-5) must be greater than -4. +# default doesn't recurse into data frames, but can customise + + Code + map_depth(x, 2, class) + Condition + Error in `.fmap()`: + i In index: 1. + Caused by error in `map_depth()`: + ! List not deep enough + # modify_depth modifies values at specified depth Code @@ -42,6 +52,20 @@ Error in `modify_depth()`: ! Negative `.depth` (-5) must be greater than -4. +# vectorised operations on the recursive and atomic levels yield same results + + Code + modify_depth(x, 5, `+`, 10L) + Condition + Error in `map()`: + i In index: 1. + Caused by error in `map()`: + i In index: 1. + Caused by error in `map()`: + i In index: 1. + Caused by error in `modify_depth()`: + ! List not deep enough + # validates depth Code diff --git a/tests/testthat/_snaps/pluck.md b/tests/testthat/_snaps/pluck.md index 65a1aeb0..43b43f33 100644 --- a/tests/testthat/_snaps/pluck.md +++ b/tests/testthat/_snaps/pluck.md @@ -277,3 +277,19 @@ Error in `pluck_raw()`: ! Index 1 must have length 1, not 26. +# pluck() dispatches on vector methods + + Code + chuck(x, 1, 1) + Condition + Error in `chuck()`: + ! Length of S3 object must be a scalar integer. + +--- + + Code + chuck(x, 1, "b", 1) + Condition + Error in `chuck()`: + ! Index 2 is attempting to pluck from an unnamed vector using a string name. + diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R index d5f31f5b..86e47008 100644 --- a/tests/testthat/test-list-combine.R +++ b/tests/testthat/test-list-combine.R @@ -70,6 +70,24 @@ test_that("NULLs are ignored", { expect_equal(list_cbind(list(df1, NULL, df2)), vec_cbind(df1, df2)) }) +test_that("NULLs are converted to NA when keep_empty = TRUE", { + df1 <- data.frame(x = 1) + df2 <- data.frame(y = 1) + + expect_equal( + list_c(list(1, NULL, 2), keep_empty = TRUE), + c(1, NA, 2) + ) + expect_equal( + list_rbind(list(df1, NULL, df1), keep_empty = TRUE), + data.frame(x = c(1, NA, 1)) + ) + expect_equal( + list_cbind(list(df1, z = NULL, df2), keep_empty = TRUE), + data.frame(df1, z = NA, df2) + ) +}) + test_that("empty inputs return expected output", { expect_equal(list_c(list()), NULL) expect_equal(list_c(list(NULL)), NULL)