From 79cc196cb461aaa2916695537c32aff777ea05dd Mon Sep 17 00:00:00 2001 From: Anatoliy Sokolov Date: Thu, 15 Aug 2024 17:18:19 -0400 Subject: [PATCH] Adding keep_empty argument to list_c, list_cbind, and list_rbind. Fixes #1096. --- NEWS.md | 2 ++ R/list-combine.R | 23 ++++++++++++++++++----- man/list_c.Rd | 15 ++++++++++----- tests/testthat/test-list-combine.R | 9 +++++++++ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2e286b62..9f82e7b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # 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 dataframe. (#1096) + # purrr 1.0.2 * Fixed valgrind issue. diff --git a/R/list-combine.R b/R/list-combine.R index ea31d5de..ff1e0390 100644 --- a/R/list-combine.R +++ b/R/list-combine.R @@ -22,6 +22,7 @@ #' 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 to keep empty elements of a list as NA. #' @inheritParams rlang::args_dots_empty #' @export #' @examples @@ -30,17 +31,21 @@ #' #' x2 <- list( #' a = data.frame(x = 1:2), -#' b = data.frame(y = "a") +#' b = data.frame(y = "a"), +#' c = data.frame(z = 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_c <- function(x, ..., ptype = NULL, keep_empty = FALSE) { vec_check_list(x) check_dots_empty() + if(keep_empty) x <- convert_empty_element_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 x <- unname(x) @@ -58,19 +63,22 @@ 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_empty_element_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_empty_element_to_NA(x) vec_rbind(!!!x, .names_to = names_to, .ptype = ptype, .error_call = current_env()) } @@ -95,3 +103,8 @@ check_list_of_data_frames <- function(x, error_call = caller_env()) { call = error_call ) } + +## used to convert empty elements into NA for list_binding functions +convert_empty_element_to_NA = function(x) { + map(x, \(x) if(vctrs::vec_is_empty(x)) NA else x) +} diff --git a/man/list_c.Rd b/man/list_c.Rd index d4f6167c..b7c1f326 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,8 @@ 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 to keep empty elements of a list as 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 +56,13 @@ list_c(x1) x2 <- list( a = data.frame(x = 1:2), - b = data.frame(y = "a") + b = data.frame(y = "a"), + c = data.frame(z = 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) + } diff --git a/tests/testthat/test-list-combine.R b/tests/testthat/test-list-combine.R index d5f31f5b..d1206f6b 100644 --- a/tests/testthat/test-list-combine.R +++ b/tests/testthat/test-list-combine.R @@ -70,6 +70,15 @@ 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), vec_rbind(df1, NA, df1)) + expect_equal(list_cbind(list(df1, z = NULL, df2), keep_empty = TRUE), vec_cbind(df1, z = NA, df2)) +}) + test_that("empty inputs return expected output", { expect_equal(list_c(list()), NULL) expect_equal(list_c(list(NULL)), NULL)