2828# ' @param data A data frame.
2929# ' @param cols <[`tidy-select`][tidyr_tidy_select]> Columns to chop or unchop.
3030# '
31- # ' If not supplied for `chop()`, then `cols` is derived as all columns _not_
32- # ' selected by `by`.
31+ # ' For `chop()`, columns specified by `by` are removed from `data` before
32+ # ' evaluating `cols`. If not supplied, `cols` is derived as all columns _not_
33+ # ' selected by `by`. At least one of `cols` and `by` must be specified.
3334# '
3435# ' For `unchop()`, each column should be a list-column containing generalised
3536# ' vectors (e.g. any mix of `NULL`s, atomic vector, S3 vectors, a lists,
3637# ' or data frames).
37- # ' @param by <[`tidy-select`][tidyr_tidy_select]> Columns to chop _by_; these
38- # ' will not be chopped.
39- # '
40- # ' `by` can be used in place of or in conjunction with columns supplied
41- # ' through `cols`.
38+ # ' @param by <[`tidy-select`][tidyr_tidy_select]> Columns to chop _by_.
4239# '
4340# ' If not supplied, then `by` is derived as all columns _not_ selected by
44- # ' `cols`.
41+ # ' `cols`. At least one of `cols` and `by` must be specified.
4542# ' @param keep_empty By default, you get one row of output for each element
4643# ' of the list that you are unchopping/unnesting. This means that if there's a
4744# ' size-0 element (like `NULL` or an empty data frame or vector), then that
5956# ' # Note that we get one row of output for each unique combination of
6057# ' # non-chopped variables
6158# ' df %>% chop(c(y, z))
62- # ' # cf nest
59+ # ' # Compare to ` nest()`
6360# ' df %>% nest(data = c(y, z))
6461# '
6562# ' # Specify variables to chop by (rather than variables to chop) using `by`
6663# ' df %>% chop(by = x)
64+ # ' # Compare to `nest()`
65+ # ' df %>% nest(.by = x)
6766# '
68- # ' # Use tidyselect syntax and helpers, just like in `dplyr::select()`
69- # ' df %>% chop(any_of(c("y", "z")))
70- # '
71- # ' # `cols` and `by` can be used together to drop columns you no longer need,
72- # ' # or to chop the columns you are chopping by too.
67+ # ' # `cols` and `by` can be used together to drop columns you no longer need.
7368# ' # This drops `z`:
74- # ' df %>% chop(y, by = x)
75- # ' # This includes `x` in the chopped columns:
76- # ' df %>% chop(everything(), by = x)
69+ # ' df %>% chop(cols = y, by = x)
70+ # '
71+ # ' # You cannot chop a column you are also trying to chop by
72+ # ' try(df %>% chop(cols = x, by = x))
7773# '
7874# ' # Unchop --------------------------------------------------------------------
7975# ' df <- tibble(x = 1:4, y = list(integer(), 1L, 1:2, 1:3))
8985# ' df <- tibble(x = 1:3, y = list(NULL, tibble(x = 1), tibble(y = 1:2)))
9086# ' df %>% unchop(y)
9187# ' df %>% unchop(y, keep_empty = TRUE)
92- chop <- function (data , cols = NULL , ... , by = NULL , error_call = current_env()) {
88+ chop <- function (
89+ data ,
90+ cols = NULL ,
91+ ... ,
92+ by = NULL ,
93+ error_call = current_env()
94+ ) {
9395 check_dots_empty0(... )
9496 check_data_frame(data , call = error_call )
9597
96- info <- chop_info(data , cols = {{ cols }}, by = {{ by }})
98+ info <- chop_info(
99+ data ,
100+ cols = {{ cols }},
101+ by = {{ by }},
102+ error_call = error_call
103+ )
97104 cols <- info $ cols
98105 by <- info $ by
99106
@@ -114,44 +121,56 @@ chop <- function(data, cols = NULL, ..., by = NULL, error_call = current_env())
114121 reconstruct_tibble(data , out )
115122}
116123
117- chop_info <- function (
118- data ,
119- cols = NULL ,
120- by = NULL ,
121- error_call = caller_env()
122- ) {
124+ chop_info <- function (data , cols , by , error_call ) {
123125 by <- enquo(by )
124- cols <- enquo( cols )
126+ has_by <- ! quo_is_null( by )
125127
126- cols_is_null <- quo_is_null (cols )
127- by_is_null <- quo_is_null(by )
128+ cols <- enquo (cols )
129+ has_cols <- ! quo_is_null(cols )
128130
129- if (cols_is_null && by_is_null ) {
130- stop_use_cols_or_by(error_call = error_call )
131+ if (! has_cols && ! has_by ) {
132+ cli :: cli_abort(
133+ " At least one of {.var cols} or {.var by} must be supplied." ,
134+ call = error_call
135+ )
131136 }
132137
133138 names <- names(data )
134139
135- cols <- names(tidyselect :: eval_select(
136- expr = cols ,
137- data = data ,
138- allow_rename = FALSE ,
139- error_call = error_call
140- ))
140+ if (has_by ) {
141+ by <- names(tidyselect :: eval_select(
142+ expr = by ,
143+ data = data ,
144+ allow_rename = FALSE ,
145+ error_call = error_call
146+ ))
147+ } else {
148+ by <- character ()
149+ }
141150
142- by <- names(tidyselect :: eval_select(
143- expr = by ,
144- data = data ,
145- allow_rename = FALSE ,
146- error_call = error_call
147- ))
151+ if (has_cols ) {
152+ # Remove `by` names before evaluating `cols`. This:
153+ # - Avoids double selection like `chop(cols = x, by = x)`
154+ # - Enables a meaningful `chop(cols = everything(), by = x)`
155+ # Consistent with `pivot_wider(id_cols = )`.
156+
157+ # TODO!: Improve on error with rethrow after rebase on main
158+ cols <- names(tidyselect :: eval_select(
159+ expr = cols ,
160+ data = data [setdiff(names , by )],
161+ allow_rename = FALSE ,
162+ error_call = error_call
163+ ))
164+ } else {
165+ cols <- character ()
166+ }
148167
149- if (cols_is_null ) {
168+ if (! has_cols ) {
150169 # Derive `cols` names from `by`
151170 cols <- setdiff(names , by )
152171 }
153172
154- if (by_is_null ) {
173+ if (! has_by ) {
155174 # Derive `by` names from `cols`
156175 by <- setdiff(names , cols )
157176 }
@@ -162,11 +181,6 @@ chop_info <- function(
162181 )
163182}
164183
165- stop_use_cols_or_by <- function (error_call = caller_env()) {
166- message <- c(" At least one of {.var cols} or {.var by} must be supplied." )
167- cli :: cli_abort(message , call = error_call )
168- }
169-
170184col_chop <- function (x , indices ) {
171185 ptype <- vec_ptype(x )
172186
0 commit comments