diff --git a/NEWS.md b/NEWS.md index 9a1965af..4c806413 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # tidyr (development version) +* `chop()` gains a `by` argument for specifying grouping columns, similar to `nest(.by =)` (@hrryt, #1490). + +* Specifying `chop()`'s `cols` argument by position is soft-deprecated. It must instead be specified by name, which better communicates intent now that `chop()` also has a `by` argument (#1490). + # tidyr 1.3.2 * `fill()` gains a `.by` argument as an alternative to `dplyr::group_by()` for diff --git a/R/chop.R b/R/chop.R index 377e1586..9d4a789b 100644 --- a/R/chop.R +++ b/R/chop.R @@ -2,17 +2,16 @@ #' #' @description #' Chopping and unchopping preserve the width of a data frame, changing its -#' length. `chop()` makes `df` shorter by converting rows within each group -#' into list-columns. `unchop()` makes `df` longer by expanding list-columns +#' length. `chop()` makes `data` shorter by converting rows within each group +#' into list-columns. `unchop()` makes `data` longer by expanding list-columns #' so that each element of the list-column gets its own row in the output. +#' #' `chop()` and `unchop()` are building blocks for more complicated functions -#' (like [unnest()], [unnest_longer()], and [unnest_wider()]) and are generally -#' more suitable for programming than interactive data analysis. +#' (like [unnest()], [unnest_longer()], and [unnest_wider()]). #' #' @details -#' Generally, unchopping is more useful than chopping because it simplifies -#' a complex data structure, and [nest()]ing is usually more appropriate -#' than `chop()`ing since it better preserves the connections between +#' When multiple columns are being chopped at once, [nest()] is usually more +#' appropriate than `chop()` since it better preserves the connections between #' observations. #' #' `chop()` creates list-columns of class [vctrs::list_of()] to ensure @@ -22,15 +21,49 @@ #' the type of its elements, `unchop()` is able to reconstitute the #' correct vector type even for empty list-columns. #' +#' @section Connection to `split()`: +#' +#' `chop()` is the tidyverse version of [base::split()], with a few key changes: +#' +#' - The unique values of the columns used to chop by are preserved losslessly +#' as output columns, rather than being converted to character labels used as +#' names on the output list. This is particularly useful when chopping by +#' non-string columns or multiple columns. +#' +#' - Multiple columns can be chopped at once, producing one list-column per +#' chopped column. The closest `split()` equivalent is to split a data frame, +#' which produces a result more similar to [nest()] than `chop()`. +#' +#' - When chopping by multiple columns, only the combinations present in the +#' data are included in the output. This is different from `split()`, which +#' takes the [interaction()] of the columns, leading to a potential +#' combinatorial explosion of output elements. +#' +#' For an even lower-level version, see [vctrs::vec_split()]. +#' #' @inheritParams rlang::args_dots_empty #' @inheritParams rlang::args_error_context #' #' @param data A data frame. -#' @param cols <[`tidy-select`][tidyr_tidy_select]> Columns to chop or unchop. +#' @param cols,by <[`tidy-select`][tidyr_tidy_select]> Column selectors. +#' +#' For `chop()`: +#' +#' - `by` selects columns to chop by. If not specified, will be derived as +#' all columns not selected by `cols`. #' -#' For `unchop()`, each column should be a list-column containing generalised -#' vectors (e.g. any mix of `NULL`s, atomic vector, S3 vectors, a lists, -#' or data frames). +#' - `cols` selects columns to chop. If not specified, will be derived as all +#' columns not selected by `by`. +#' +#' Specifying both `by` and `cols` drops all unselected columns in `data` from +#' the output. Note that columns selected by `by` are removed from `data` +#' before evaluating `cols`. +#' +#' At least one of `by` or `cols` must be specified. +#' +#' For `unchop()`, `cols` selects columns to unchop. Each column should be a +#' list-column containing generalised vectors (e.g. any mix of `NULL`s, atomic +#' vectors, S3 vectors, lists, or data frames). #' @param keep_empty By default, you get one row of output for each element #' of the list that you are unchopping/unnesting. This means that if there's a #' size-0 element (like `NULL` or an empty data frame or vector), then that @@ -44,41 +77,74 @@ #' @export #' @examples #' # Chop ---------------------------------------------------------------------- -#' df <- tibble(x = c(1, 1, 1, 2, 2, 3), y = 1:6, z = 6:1) -#' # Note that we get one row of output for each unique combination of -#' # non-chopped variables -#' df |> chop(c(y, z)) -#' # cf nest -#' df |> nest(data = c(y, z)) +#' df <- tibble(x = c(1, 1, 1, 2, 2, 3), y = c(1, 1, 2, 3, 3, 4), z = 1:6) +#' +#' # `chop()` is most useful as a tidyverse alternative to `base::split()` +#' +#' # Chop `z` by `x` and `y`. Note that we get one row of output for each unique +#' # combination of variables that we chop by. +#' df |> chop(by = c(x, y)) +#' +#' # Compare to `split()`, notice how `x` and `y` are converted to character +#' # labels +#' df |> split(df[c("x", "y")], drop = TRUE) +#' +#' # Equivalently, specify variables to chop (rather than variables to chop by) +#' df |> chop(cols = z) +#' +#' # `cols` and `by` can be used together to drop columns you no longer need. +#' # This drops `y`: +#' df |> chop(cols = z, by = x) +#' +#' # You cannot chop a column you are also trying to chop by +#' try(df |> chop(cols = x, by = x)) +#' +#' # Multiple columns can be chopped at once, producing one list-column per +#' # chopped column +#' df |> chop(by = x) +#' # Compare to `nest()`, which keeps the chopped `y` and `z` columns together +#' # in nested data frames +#' df |> nest(.by = x) +#' # `split()` is more similar to `nest()` here +#' split(df[c("y", "z")], df["x"]) #' #' # Unchop -------------------------------------------------------------------- #' df <- tibble(x = 1:4, y = list(integer(), 1L, 1:2, 1:3)) #' df |> unchop(y) #' df |> unchop(y, keep_empty = TRUE) #' -#' # unchop will error if the types are not compatible: +#' # `unchop()` will error if the types are not compatible: #' df <- tibble(x = 1:2, y = list("1", 1:3)) #' try(df |> unchop(y)) #' #' # Unchopping a list-col of data frames must generate a df-col because -#' # unchop leaves the column names unchanged +#' # `unchop()` leaves the column names unchanged #' df <- tibble(x = 1:3, y = list(NULL, tibble(x = 1), tibble(y = 1:2))) #' df |> unchop(y) #' df |> unchop(y, keep_empty = TRUE) -chop <- function(data, cols, ..., error_call = current_env()) { - check_dots_empty0(...) +chop <- function( + data, + ..., + cols = NULL, + by = NULL, + error_call = current_env() +) { check_data_frame(data, call = error_call) - check_required(cols, call = error_call) - cols <- tidyselect::eval_select( - expr = enquo(cols), - data = data, - allow_rename = FALSE, + cols <- compat_chop_cols(cols = enquo(cols), ...) + by <- enquo(by) + + info <- chop_info( + data, + cols = !!cols, + by = !!by, error_call = error_call ) + cols <- info$cols + by <- info$by cols <- tidyr_new_list(data[cols]) - keys <- data[setdiff(names(data), names(cols))] + keys <- data[by] info <- vec_group_loc(keys) keys <- info$key @@ -94,6 +160,77 @@ chop <- function(data, cols, ..., error_call = current_env()) { reconstruct_tibble(data, out) } +chop_info <- function(data, cols, by, error_call) { + by <- enquo(by) + has_by <- !quo_is_null(by) + + cols <- enquo(cols) + has_cols <- !quo_is_null(cols) + + if (!has_cols && !has_by) { + cli::cli_abort( + "At least one of {.var cols} or {.var by} must be supplied.", + call = error_call + ) + } + + names <- names(data) + + if (has_by) { + by <- names(tidyselect::eval_select( + expr = by, + data = data, + allow_rename = FALSE, + error_call = error_call + )) + } else { + by <- character() + } + + if (has_cols) { + # Remove `by` names before evaluating `cols`. This: + # - Avoids double selection like `chop(cols = x, by = x)`, which would cause + # name collisions otherwise. + # - Enables a meaningful `chop(cols = everything(), by = x)`. + # Consistent with `pivot_wider(id_cols = )`. + try_fetch( + cols <- names(tidyselect::eval_select( + expr = cols, + data = data[setdiff(names, by)], + allow_rename = FALSE, + error_call = error_call + )), + vctrs_error_subscript_oob = function(cnd) { + maybe_throw_already_selected_error( + cnd[["i"]], + "cols", + by, + "by", + error_call + ) + zap() + } + ) + } else { + cols <- character() + } + + if (!has_cols) { + # Derive `cols` names from `by` + cols <- setdiff(names, by) + } + + if (!has_by) { + # Derive `by` names from `cols` + by <- setdiff(names, cols) + } + + list( + cols = cols, + by = by + ) +} + col_chop <- function(x, indices) { ptype <- vec_ptype(x) @@ -103,6 +240,49 @@ col_chop <- function(x, indices) { out } +compat_chop_cols <- function(cols, ...) { + n_dots <- dots_n(...) + + if (n_dots == 0L) { + return(cols) + } + + # `env` and `user_env` here are fixed to always report `chop()` as `env` + # and the caller of `chop()` as `user_env`, regardless of the `error_call` + # argument. We think that makes the most sense for these errors/warnings. + env <- caller_env() + user_env <- caller_env(2) + + if (n_dots != 1L) { + check_dots_empty0(..., call = env) + } + + if (!quo_is_null(cols)) { + cli::cli_abort( + "Can't specify `cols` by both name and position.", + call = env + ) + } + + # Safe, we checked `n_dots == 1L` above + cols <- enquos(...)[[1L]] + + lifecycle::deprecate_soft( + when = "1.4.0", + what = I(cli::format_inline( + "Specifying the {.arg cols} argument by position" + )), + details = cli::format_inline( + "Please explicitly name {.arg cols}, like {.code chop(data, cols = {as_label(cols)})}." + ), + env = env, + user_env = user_env, + id = "tidyr-chop-positional-cols" + ) + + cols +} + #' @export #' @rdname chop unchop <- function( diff --git a/R/pivot-wide.R b/R/pivot-wide.R index 591ac748..2e8be722 100644 --- a/R/pivot-wide.R +++ b/R/pivot-wide.R @@ -643,34 +643,54 @@ select_wider_id_cols <- function( error_call = error_call ), vctrs_error_subscript_oob = function(cnd) { - rethrow_id_cols_oob(cnd, names_from_cols, values_from_cols, error_call) + maybe_throw_already_selected_error( + cnd[["i"]], + "id_cols", + names_from_cols, + "names_from", + error_call + ) + maybe_throw_already_selected_error( + cnd[["i"]], + "id_cols", + values_from_cols, + "values_from", + error_call + ) + zap() } ) names(id_cols) } -rethrow_id_cols_oob <- function(cnd, names_from_cols, values_from_cols, call) { - i <- cnd[["i"]] +maybe_throw_already_selected_error <- function( + new_cols, + new_arg, + old_cols, + old_arg, + call +) { + if (!is_character(new_cols)) { + # Let someone else handle it + return() + } - if (is_string(i)) { - # Try to throw our custom error - if (i %in% names_from_cols) { - stop_id_cols_oob(i, "names_from", call = call) - } else if (i %in% values_from_cols) { - stop_id_cols_oob(i, "values_from", call = call) + # Try to throw our custom error + for (new_col in new_cols) { + if (new_col %in% old_cols) { + stop_already_selected(new_col, new_arg, old_arg, call) } } - # Otherwise fall through and throw standard tidyselect error - zap() + # Let someone else handle it } -stop_id_cols_oob <- function(i, arg, call) { +stop_already_selected <- function(col, new_arg, old_arg, call) { cli::cli_abort( c( - "`id_cols` can't select a column already selected by `{arg}`.", - i = "Column `{i}` has already been selected." + "{.code {new_arg}} can't reference a column already selected by {.code {old_arg}}.", + i = "Column {.code {col}} has already been selected." ), parent = NA, call = call diff --git a/man/chop.Rd b/man/chop.Rd index a0bea90c..8f72409b 100644 --- a/man/chop.Rd +++ b/man/chop.Rd @@ -5,7 +5,7 @@ \alias{unchop} \title{Chop and unchop} \usage{ -chop(data, cols, ..., error_call = current_env()) +chop(data, ..., cols = NULL, by = NULL, error_call = current_env()) unchop( data, @@ -19,13 +19,27 @@ unchop( \arguments{ \item{data}{A data frame.} -\item{cols}{<\code{\link[=tidyr_tidy_select]{tidy-select}}> Columns to chop or unchop. +\item{...}{These dots are for future extensions and must be empty.} -For \code{unchop()}, each column should be a list-column containing generalised -vectors (e.g. any mix of \code{NULL}s, atomic vector, S3 vectors, a lists, -or data frames).} +\item{cols, by}{<\code{\link[=tidyr_tidy_select]{tidy-select}}> Column selectors. -\item{...}{These dots are for future extensions and must be empty.} +For \code{chop()}: +\itemize{ +\item \code{by} selects columns to chop by. If not specified, will be derived as +all columns not selected by \code{cols}. +\item \code{cols} selects columns to chop. If not specified, will be derived as all +columns not selected by \code{by}. +} + +Specifying both \code{by} and \code{cols} drops all unselected columns in \code{data} from +the output. Note that columns selected by \code{by} are removed from \code{data} +before evaluating \code{cols}. + +At least one of \code{by} or \code{cols} must be specified. + +For \code{unchop()}, \code{cols} selects columns to unchop. Each column should be a +list-column containing generalised vectors (e.g. any mix of \code{NULL}s, atomic +vectors, S3 vectors, lists, or data frames).} \item{error_call}{The execution environment of a currently running function, e.g. \code{caller_env()}. The function will be @@ -46,17 +60,16 @@ can be supplied, which will be applied to all \code{cols}.} } \description{ Chopping and unchopping preserve the width of a data frame, changing its -length. \code{chop()} makes \code{df} shorter by converting rows within each group -into list-columns. \code{unchop()} makes \code{df} longer by expanding list-columns +length. \code{chop()} makes \code{data} shorter by converting rows within each group +into list-columns. \code{unchop()} makes \code{data} longer by expanding list-columns so that each element of the list-column gets its own row in the output. + \code{chop()} and \code{unchop()} are building blocks for more complicated functions -(like \code{\link[=unnest]{unnest()}}, \code{\link[=unnest_longer]{unnest_longer()}}, and \code{\link[=unnest_wider]{unnest_wider()}}) and are generally -more suitable for programming than interactive data analysis. +(like \code{\link[=unnest]{unnest()}}, \code{\link[=unnest_longer]{unnest_longer()}}, and \code{\link[=unnest_wider]{unnest_wider()}}). } \details{ -Generally, unchopping is more useful than chopping because it simplifies -a complex data structure, and \code{\link[=nest]{nest()}}ing is usually more appropriate -than \code{chop()}ing since it better preserves the connections between +When multiple columns are being chopped at once, \code{\link[=nest]{nest()}} is usually more +appropriate than \code{chop()} since it better preserves the connections between observations. \code{chop()} creates list-columns of class \code{\link[vctrs:list_of]{vctrs::list_of()}} to ensure @@ -66,26 +79,71 @@ the roundtrip chop and unchop. Because \verb{} keeps tracks of the type of its elements, \code{unchop()} is able to reconstitute the correct vector type even for empty list-columns. } +\section{Connection to \code{split()}}{ + + +\code{chop()} is the tidyverse version of \code{\link[base:split]{base::split()}}, with a few key changes: +\itemize{ +\item The unique values of the columns used to chop by are preserved losslessly +as output columns, rather than being converted to character labels used as +names on the output list. This is particularly useful when chopping by +non-string columns or multiple columns. +\item Multiple columns can be chopped at once, producing one list-column per +chopped column. The closest \code{split()} equivalent is to split a data frame, +which produces a result more similar to \code{\link[=nest]{nest()}} than \code{chop()}. +\item When chopping by multiple columns, only the combinations present in the +data are included in the output. This is different from \code{split()}, which +takes the \code{\link[=interaction]{interaction()}} of the columns, leading to a potential +combinatorial explosion of output elements. +} + +For an even lower-level version, see \code{\link[vctrs:vec_split]{vctrs::vec_split()}}. +} + \examples{ # Chop ---------------------------------------------------------------------- -df <- tibble(x = c(1, 1, 1, 2, 2, 3), y = 1:6, z = 6:1) -# Note that we get one row of output for each unique combination of -# non-chopped variables -df |> chop(c(y, z)) -# cf nest -df |> nest(data = c(y, z)) +df <- tibble(x = c(1, 1, 1, 2, 2, 3), y = c(1, 1, 2, 3, 3, 4), z = 1:6) + +# `chop()` is most useful as a tidyverse alternative to `base::split()` + +# Chop `z` by `x` and `y`. Note that we get one row of output for each unique +# combination of variables that we chop by. +df |> chop(by = c(x, y)) + +# Compare to `split()`, notice how `x` and `y` are converted to character +# labels +df |> split(df[c("x", "y")], drop = TRUE) + +# Equivalently, specify variables to chop (rather than variables to chop by) +df |> chop(cols = z) + +# `cols` and `by` can be used together to drop columns you no longer need. +# This drops `y`: +df |> chop(cols = z, by = x) + +# You cannot chop a column you are also trying to chop by +try(df |> chop(cols = x, by = x)) + +# Multiple columns can be chopped at once, producing one list-column per +# chopped column +df |> chop(by = x) +# Compare to `nest()`, which keeps the chopped `y` and `z` columns together +# in nested data frames +df |> nest(.by = x) +# `split()` is more similar to `nest()` here +split(df[c("y", "z")], df["x"]) # Unchop -------------------------------------------------------------------- df <- tibble(x = 1:4, y = list(integer(), 1L, 1:2, 1:3)) df |> unchop(y) df |> unchop(y, keep_empty = TRUE) -# unchop will error if the types are not compatible: +# `unchop()` will error if the types are not compatible: df <- tibble(x = 1:2, y = list("1", 1:3)) try(df |> unchop(y)) # Unchopping a list-col of data frames must generate a df-col because -# unchop leaves the column names unchanged +# `unchop()` leaves the column names unchanged df <- tibble(x = 1:3, y = list(NULL, tibble(x = 1), tibble(y = 1:2))) df |> unchop(y) df |> unchop(y, keep_empty = TRUE) diff --git a/tests/testthat/_snaps/chop.md b/tests/testthat/_snaps/chop.md index 9292d453..ab12367d 100644 --- a/tests/testthat/_snaps/chop.md +++ b/tests/testthat/_snaps/chop.md @@ -12,7 +12,74 @@ chop(df) Condition Error in `chop()`: - ! `cols` is absent but must be supplied. + ! At least one of `cols` or `by` must be supplied. + +# can't select same column in `by` and `cols` (#1490) + + Code + chop(df, cols = x, by = x) + Condition + Error in `chop()`: + ! `cols` can't reference a column already selected by `by`. + i Column `x` has already been selected. + +# must supply at least one of `by` or `cols` + + Code + chop(df) + Condition + Error in `chop()`: + ! At least one of `cols` or `by` must be supplied. + +# specifying `cols` by position is deprecated + + Code + out <- chop(df, x) + Condition + Warning: + Specifying the `cols` argument by position was deprecated in tidyr 1.4.0. + i Please explicitly name `cols`, like `chop(data, cols = x)`. + +--- + + Code + out <- chop(df, x, by = y) + Condition + Warning: + Specifying the `cols` argument by position was deprecated in tidyr 1.4.0. + i Please explicitly name `cols`, like `chop(data, cols = x)`. + +--- + + Code + chop(df, x, y) + Condition + Error in `chop()`: + ! `...` must be empty. + x Problematic arguments: + * ..1 = x + * ..2 = y + i Did you forget to name an argument? + +--- + + Code + chop(df, x, cols = y) + Condition + Error in `chop()`: + ! Can't specify `cols` by both name and position. + +--- + + Code + my_chop() + Condition + Error in `chop()`: + ! `...` must be empty. + x Problematic arguments: + * ..1 = x + * ..2 = y + i Did you forget to name an argument? # incompatible ptype mentions the column (#1477) diff --git a/tests/testthat/_snaps/pivot-wide.md b/tests/testthat/_snaps/pivot-wide.md index cbaae558..a81d4b5e 100644 --- a/tests/testthat/_snaps/pivot-wide.md +++ b/tests/testthat/_snaps/pivot-wide.md @@ -184,13 +184,13 @@ Error in `build_wider_spec()`: ! `names_expand` must be `TRUE` or `FALSE`, not the string "x". -# `id_cols` can't select columns from `names_from` or `values_from` (#1318) +# `id_cols` can't select columns from `names_from` or `values_from` (#1318, #1506) Code pivot_wider(df, id_cols = name, names_from = name, values_from = value) Condition Error in `pivot_wider()`: - ! `id_cols` can't select a column already selected by `names_from`. + ! `id_cols` can't reference a column already selected by `names_from`. i Column `name` has already been selected. --- @@ -199,7 +199,25 @@ pivot_wider(df, id_cols = value, names_from = name, values_from = value) Condition Error in `pivot_wider()`: - ! `id_cols` can't select a column already selected by `values_from`. + ! `id_cols` can't reference a column already selected by `values_from`. + i Column `value` has already been selected. + +--- + + Code + pivot_wider(df, id_cols = all_of(cols), names_from = name, values_from = value) + Condition + Error in `pivot_wider()`: + ! `id_cols` can't reference a column already selected by `names_from`. + i Column `name` has already been selected. + +--- + + Code + pivot_wider(df, id_cols = all_of(cols), names_from = name, values_from = value) + Condition + Error in `pivot_wider()`: + ! `id_cols` can't reference a column already selected by `values_from`. i Column `value` has already been selected. # `id_cols` returns a tidyselect error if a column selection is OOB (#1318) diff --git a/tests/testthat/test-chop.R b/tests/testthat/test-chop.R index 33594749..203fd795 100644 --- a/tests/testthat/test-chop.R +++ b/tests/testthat/test-chop.R @@ -2,7 +2,7 @@ test_that("can chop multiple columns", { df <- tibble(x = c(1, 1, 2), a = 1:3, b = 1:3) - out <- df |> chop(c(a, b)) + out <- df |> chop(cols = c(a, b)) expect_named(out, c("x", "a", "b")) expect_equal(out$a, list_of(1:2, 3L)) @@ -11,12 +11,20 @@ test_that("can chop multiple columns", { test_that("chopping no columns returns input", { df <- tibble(a1 = 1, a2 = 2, b1 = 1, b2 = 2) - expect_equal(chop(df, c()), df) + expect_equal(chop(df, cols = c()), df) +}) + +test_that("chopping by no columns chops all columns", { + df <- tibble(a1 = 1, a2 = 2, b1 = 1, b2 = 2) + expect_identical( + chop(df, by = c()), + chop(df, cols = c(a1, a2, b1, b2)) + ) }) test_that("grouping is preserved", { df <- tibble(g = c(1, 1), x = 1:2) - out <- df |> dplyr::group_by(g) |> chop(x) + out <- df |> dplyr::group_by(g) |> chop(cols = x) expect_equal(dplyr::group_vars(out), "g") }) @@ -34,19 +42,97 @@ test_that("can chop empty data frame (#1206)", { df <- tibble(x = integer(), y = integer()) expect_identical( - chop(df, y), + chop(df, cols = y), tibble(x = integer(), y = list_of(.ptype = integer())) ) expect_identical( - chop(df, x), + chop(df, cols = x), tibble(y = integer(), x = list_of(.ptype = integer())) ) expect_identical( - chop(df, c(x, y)), + chop(df, cols = c(x, y)), tibble(x = list_of(.ptype = integer()), y = list_of(.ptype = integer())) ) }) +test_that("can chop `by` columns (#1490)", { + df <- tibble(x = c(1, 1, 1, 2, 2), y = c(2, 1, 2, 3, 4), z = 1:5) + + expect_identical( + chop(df, by = c(x, y)), + chop(df, cols = z) + ) +}) + +test_that("can combine `by` with `cols` (#1490)", { + df <- tibble(x = c(1, 1, 1, 2, 2), y = c(2, 1, 2, 3, 4), z = 1:5) + + # `by` cols come first, then `cols` cols. Unselected cols are dropped! + expect_identical( + chop(df, cols = x, by = y), + chop(df[c("x", "y")], cols = x) + ) +}) + +test_that("`by` columns are removed before evaluating `cols` (#1490)", { + # Similar to `id_cols` in `pivot_wider()` + df <- tibble(x = 1, y = 2, by = 3) + + expect_identical( + chop(df, cols = everything(), by = by), + chop(df, cols = c(x, y)) + ) +}) + +test_that("can't select same column in `by` and `cols` (#1490)", { + df <- tibble(x = 1) + + expect_snapshot(error = TRUE, { + chop(df, cols = x, by = x) + }) +}) + +test_that("must supply at least one of `by` or `cols`", { + df <- tibble(x = 1) + expect_snapshot(error = TRUE, { + chop(df) + }) +}) + +test_that("specifying `cols` by position is deprecated", { + df <- tibble(x = 1, y = 2, z = 3) + + # Deprecation warning, but works + expect_snapshot({ + out <- chop(df, x) + }) + expect_identical(out, chop(df, cols = x)) + + # Deprecation warning, but works (with `by`) + expect_snapshot({ + out <- chop(df, x, by = y) + }) + expect_identical(out, chop(df, cols = x, by = y)) + + # Two positional `...`, empty dots error + expect_snapshot(error = TRUE, { + chop(df, x, y) + }) + + # Both positional and named, error + expect_snapshot(error = TRUE, { + chop(df, x, cols = y) + }) + + # Reports `chop()` as the call regardless of `error_call` + my_chop <- function() { + chop(df, x, y) + } + expect_snapshot(error = TRUE, { + my_chop() + }) +}) + # unchop ------------------------------------------------------------------ test_that("extends into rows", { @@ -283,7 +369,7 @@ test_that("unchopping list of empty types retains type", { }) test_that("unchop retrieves correct types with emptied chopped df", { - chopped <- chop(tibble(x = 1:3, y = 4:6), y) + chopped <- chop(tibble(x = 1:3, y = 4:6), cols = y) empty <- vec_slice(chopped, 0L) expect_identical(unchop(empty, y), tibble(x = integer(), y = integer())) }) diff --git a/tests/testthat/test-pivot-wide.R b/tests/testthat/test-pivot-wide.R index 2574d814..ee27f961 100644 --- a/tests/testthat/test-pivot-wide.R +++ b/tests/testthat/test-pivot-wide.R @@ -445,7 +445,7 @@ test_that("`id_cols = everything()` excludes `names_from` and `values_from`", { ) }) -test_that("`id_cols` can't select columns from `names_from` or `values_from` (#1318)", { +test_that("`id_cols` can't select columns from `names_from` or `values_from` (#1318, #1506)", { df <- tibble(name = c("x", "y"), value = c(1, 2)) # And gives a nice error message! @@ -455,6 +455,27 @@ test_that("`id_cols` can't select columns from `names_from` or `values_from` (#1 expect_snapshot(error = TRUE, { pivot_wider(df, id_cols = value, names_from = name, values_from = value) }) + + # With `all_of()` selecting multiple columns, we report the first matched problem + cols <- c("name", "nonexistent") + expect_snapshot(error = TRUE, { + pivot_wider( + df, + id_cols = all_of(cols), + names_from = name, + values_from = value + ) + }) + + cols <- c("value", "nonexistent") + expect_snapshot(error = TRUE, { + pivot_wider( + df, + id_cols = all_of(cols), + names_from = name, + values_from = value + ) + }) }) test_that("`id_cols` returns a tidyselect error if a column selection is OOB (#1318)", {