Skip to content

Commit 0eac5f7

Browse files
committed
Rework with new invariants
1 parent a41607e commit 0eac5f7

4 files changed

Lines changed: 123 additions & 79 deletions

File tree

R/chop.R

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,17 @@
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
@@ -59,21 +56,20 @@
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))
@@ -89,11 +85,22 @@
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-
170184
col_chop <- function(x, indices) {
171185
ptype <- vec_ptype(x)
172186

man/chop.Rd

Lines changed: 13 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/chop.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@
1414
Error in `chop()`:
1515
! At least one of `cols` or `by` must be supplied.
1616

17+
# can't select same column in `by` and `cols` (#1490)
18+
19+
Code
20+
chop(df, x, by = x)
21+
Condition
22+
Error in `chop()`:
23+
! Can't select columns that don't exist.
24+
x Column `x` doesn't exist.
25+
26+
# must supply at least one of `by` or `cols`
27+
28+
Code
29+
chop(df)
30+
Condition
31+
Error in `chop()`:
32+
! At least one of `cols` or `by` must be supplied.
33+
1734
# incompatible ptype mentions the column (#1477)
1835

1936
Code

tests/testthat/test-chop.R

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ test_that("chopping no columns returns input", {
1414
expect_equal(chop(df, c()), df)
1515
})
1616

17+
test_that("chopping by no columns chops all columns", {
18+
df <- tibble(a1 = 1, a2 = 2, b1 = 1, b2 = 2)
19+
expect_identical(
20+
chop(df, by = c()),
21+
chop(df, c(a1, a2, b1, b2))
22+
)
23+
})
24+
1725
test_that("grouping is preserved", {
1826
df <- tibble(g = c(1, 1), x = 1:2)
1927
out <- df %>% dplyr::group_by(g) %>% chop(x)
@@ -59,26 +67,35 @@ test_that("can chop `by` columns (#1490)", {
5967
test_that("can combine `by` with `cols` (#1490)", {
6068
df <- tibble(x = c(1, 1, 1, 2, 2), y = c(2, 1, 2, 3, 4), z = 1:5)
6169

70+
# `by` cols come first, then `cols` cols. Unselected cols are dropped!
6271
expect_identical(
6372
chop(df, x, by = y),
64-
chop(dplyr::select(df, -z), x)
73+
chop(df[c("x", "y")], x)
6574
)
6675
})
6776

68-
test_that("union of `by` and `cols` results in renaming (#1490)", {
69-
df <- tibble(x = 1, y = 1)
70-
one <- vctrs::list_of(1)
77+
test_that("`by` columns are removed before evaluating `cols` (#1490)", {
78+
# Similar to `id_cols` in `pivot_wider()`
79+
df <- tibble(x = 1, y = 2, by = 3)
7180

72-
with_options(rlib_name_repair_verbosity = "quiet", {
73-
expect_identical(
74-
invisible(chop(df, everything(), by = x)),
75-
tibble(x = 1, x = one, y = one, .name_repair = "unique")
76-
)
81+
expect_identical(
82+
chop(df, everything(), by = by),
83+
chop(df, c(x, y))
84+
)
85+
})
7786

78-
expect_identical(
79-
invisible(chop(df, x, by = everything())),
80-
tibble(x = 1, y = 1, x = one, .name_repair = "unique")
81-
)
87+
test_that("can't select same column in `by` and `cols` (#1490)", {
88+
df <- tibble(x = 1)
89+
90+
expect_snapshot(error = TRUE, {
91+
chop(df, x, by = x)
92+
})
93+
})
94+
95+
test_that("must supply at least one of `by` or `cols`", {
96+
df <- tibble(x = 1)
97+
expect_snapshot(error = TRUE, {
98+
chop(df)
8299
})
83100
})
84101

0 commit comments

Comments
 (0)