diff --git a/NAMESPACE b/NAMESPACE index beb426f2..bc58c2f7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -160,6 +160,7 @@ export(map_if) export(map_int) export(map_lgl) export(map_raw) +export(map_vec) export(modify) export(modify2) export(modify_at) @@ -213,4 +214,7 @@ export(zap) import(rlang) import(vctrs) importFrom(magrittr,"%>%") +importFrom(vctrs,vec_c) +importFrom(vctrs,vec_ptype_common) +importFrom(vctrs,vec_size) useDynLib(purrr, .registration = TRUE) diff --git a/R/map.R b/R/map.R index 849289d5..fde35569 100644 --- a/R/map.R +++ b/R/map.R @@ -11,6 +11,9 @@ #' * `map_lgl()`, `map_int()`, `map_dbl()` and `map_chr()` return an #' atomic vector of the indicated type (or die trying). #' +#' * `map_vec()` simplifies to the common type of the output. It works with +#' most types of simple vectors like Date, POSIXct, factors, etc. +#' #' * `map_dfr()` and `map_dfc()` return a data frame created by #' row-binding and column-binding respectively. They require dplyr #' to be installed. `map_df()` is an alias for `map_dfr()`. @@ -218,6 +221,27 @@ map_raw <- function(.x, .f, ...) { .Call(map_impl, environment(), ".x", ".f", "raw") } + +#' @rdname map +#' @param .ptype If `NULL`, the default, the output type is the common type +#' of the elements of the result. Otherwise, supply a "prototype" giving +#' the desired type of output. +#' @importFrom vctrs vec_c vec_size vec_ptype_common +#' @export +map_vec <- function(.x, .f, ..., .ptype = NULL) { + out <- map(.x, .f, ...) + + .ptype <- vec_ptype_common(!!!out, .ptype = .ptype) + for (i in seq_along(out)) { + if (vec_size(out[[i]]) != 1L) { + stop_bad_element_vector(out[[i]], i, .ptype, 1L, what = "Result") + } + } + + vec_c(!!!out, .ptype = .ptype) +} + + #' @rdname map #' @param .id Either a string or `NULL`. If a string, the output will contain #' a variable with that name, storing either the name (if `.x` is named) or diff --git a/man/map.Rd b/man/map.Rd index d471740d..b46ced66 100644 --- a/man/map.Rd +++ b/man/map.Rd @@ -7,6 +7,7 @@ \alias{map_int} \alias{map_dbl} \alias{map_raw} +\alias{map_vec} \alias{map_dfr} \alias{map_df} \alias{map_dfc} @@ -25,6 +26,8 @@ map_dbl(.x, .f, ...) map_raw(.x, .f, ...) +map_vec(.x, .f, ..., .ptype = NULL) + map_dfr(.x, .f, ..., .id = NULL) map_dfc(.x, .f, ...) @@ -59,6 +62,10 @@ present, the value of \code{.default} will be returned.} \item{...}{Additional arguments passed on to the mapped function.} +\item{.ptype}{If \code{NULL}, the default, the output type is the common type +of the elements of the result. Otherwise, supply a "prototype" giving +the desired type of output.} + \item{.id}{Either a string or \code{NULL}. If a string, the output will contain a variable with that name, storing either the name (if \code{.x} is named) or the index (if \code{.x} is unnamed) of the input. If \code{NULL}, the default, no @@ -91,6 +98,8 @@ each element of a list or atomic vector and returning an object of the same leng versions that return an object of the same type as the input. \item \code{map_lgl()}, \code{map_int()}, \code{map_dbl()} and \code{map_chr()} return an atomic vector of the indicated type (or die trying). +\item \code{map_vec()} simplifies to the common type of the output. It works with +most types of simple vectors like Date, POSIXct, factors, etc. \item \code{map_dfr()} and \code{map_dfc()} return a data frame created by row-binding and column-binding respectively. They require dplyr to be installed. \code{map_df()} is an alias for \code{map_dfr()}. diff --git a/tests/testthat/_snaps/map.md b/tests/testthat/_snaps/map.md new file mode 100644 index 00000000..303b290c --- /dev/null +++ b/tests/testthat/_snaps/map.md @@ -0,0 +1,24 @@ +# requires output be length 1 + + Code + map_vec(1:2, ~ rep(1, .x)) + Condition + Error in `stop_bad_type()`: + ! Result 2 must be a single double, not a double vector of length 2 + +# requires common type of output + + Code + map_vec(1:2, ~ if (.x == 1) factor("x") else 1) + Condition + Error in `map_vec()`: + ! Can't combine `..1` > and `..2` . + +# can enforce .ptype + + Code + map_vec(1:2, ~ factor("x"), .ptype = integer()) + Condition + Error: + ! Can't convert > to . + diff --git a/tests/testthat/test-map.R b/tests/testthat/test-map.R index d832fcda..b2e8c814 100644 --- a/tests/testthat/test-map.R +++ b/tests/testthat/test-map.R @@ -170,3 +170,32 @@ test_that("map() with empty input copies names", { expect_identical(map_chr(named_list, identity), named(chr())) expect_identical(map_raw(named_list, identity), named(raw())) }) + + +# map_vec ----------------------------------------------------------------- + +test_that("still iterates using [[", { + df <- data.frame(x = 1, y = 2, z = 3) + expect_equal(map_vec(df, length), c(x = 1, y = 1, z = 1)) +}) + +test_that("requires output be length 1", { + expect_snapshot(error = TRUE, { + map_vec(1:2, ~ rep(1, .x)) + }) +}) + +test_that("requires common type of output", { + out <- map_vec(1:2, ~ factor("x")) + expect_equal(out, factor(c("x", "x"))) + + expect_snapshot(error = TRUE, { + map_vec(1:2, ~ if (.x == 1) factor("x") else 1) + }) +}) + +test_that("can enforce .ptype", { + expect_snapshot(error = TRUE, { + map_vec(1:2, ~ factor("x"), .ptype = integer()) + }) +})