Skip to content

Commit

Permalink
Fix if_any() and if_all() behavior with zero-column selections (#…
Browse files Browse the repository at this point in the history
…7072)

* Fix `if_any()` and `if_all()` behavior with zero-column selections

Fixes #7059

This commit addresses the unexpected behavior of `if_any()` and `if_all()` when no columns are selected. Specifically, `if_any()` now returns `FALSE`, and `if_all()` returns `TRUE` in cases where no columns match the selection criteria.

Additionally, tests have been added to ensure the correct behavior of these functions in both `filter()` and `mutate()` contexts, including the scenarios highlighted by the `reprex` example. This ensures that empty selections behave consistently with the logical expectations.

* Update documentation

- Moved specific details into the `@details` section of the documentation.
- Ran `devtools::document()` to update Rd files.
- Added a bullet in `NEWS.md` to highlight the consistent behavior of `if_any()` and `if_all()` with `any()` and `all()` when called with empty inputs.

* Fix handling of `!!` in `expand_if_across()`

* Simplified expected values in tests and adjusted test descriptions to reference issue #7059

* Revert `select.Rd` changes, not sure what's going on here

---------

Co-authored-by: Davis Vaughan <davis@posit.co>
  • Loading branch information
jrwinget and DavisVaughan committed Aug 15, 2024
1 parent 663d7f0 commit b2c7e04
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 10 deletions.
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

* R >=3.6.0 is now explicitly required (#7026).

* `if_any()` and `if_all()` are now fully consistent with `any()` and `all()`.
In particular, when called with empty inputs `if_any()` returns `FALSE` and
`if_all()` returns `TRUE` (#7059, @jrwinget).

# dplyr 1.1.4

* `join_by()` now allows its helper functions to be namespaced with `dplyr::`,
Expand Down
34 changes: 28 additions & 6 deletions R/across.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#' `across()` makes it easy to apply the same transformation to multiple
#' columns, allowing you to use [select()] semantics inside in "data-masking"
#' functions like [summarise()] and [mutate()]. See `vignette("colwise")` for
#' more details.
#' more details.
#'
#' `if_any()` and `if_all()` apply the same
#' predicate function to a selection of columns and combine the
Expand All @@ -18,6 +18,14 @@
#' `across()` supersedes the family of "scoped variants" like
#' `summarise_at()`, `summarise_if()`, and `summarise_all()`.
#'
#' @details
#' When there are no selected columns:
#'
#' - `if_any()` will return `FALSE`, consistent with the behavior of
#' `any()` when called without inputs.
#' - `if_all()` will return `TRUE`, consistent with the behavior of
#' `all()` when called without inputs.
#'
#' @param .cols <[`tidy-select`][dplyr_tidy_select]> Columns to transform.
#' You can't select grouping columns because they are already automatically
#' handled by the verb (i.e. [summarise()] or [mutate()]).
Expand Down Expand Up @@ -133,9 +141,16 @@
#' iris %>%
#' group_by(Species) %>%
#' summarise(across(starts_with("Sepal"), mean, .names = "mean_{.col}"))
#'
#' iris %>%
#' group_by(Species) %>%
#' summarise(across(starts_with("Sepal"), list(mean = mean, sd = sd), .names = "{.col}.{.fn}"))
#' summarise(
#' across(
#' starts_with("Sepal"),
#' list(mean = mean, sd = sd),
#' .names = "{.col}.{.fn}"
#' )
#' )
#'
#' # If a named external vector is used for column selection, .names will use
#' # those names when constructing the output names
Expand All @@ -146,7 +161,9 @@
#' # When the list is not named, .fn is replaced by the function's position
#' iris %>%
#' group_by(Species) %>%
#' summarise(across(starts_with("Sepal"), list(mean, sd), .names = "{.col}.fn{.fn}"))
#' summarise(
#' across(starts_with("Sepal"), list(mean, sd), .names = "{.col}.fn{.fn}")
#' )
#'
#' # When the functions in .fns return a data frame, you typically get a
#' # "packed" data frame back
Expand All @@ -164,7 +181,9 @@
#'
#' # .unpack can utilize a glue specification if you don't like the defaults
#' iris %>%
#' reframe(across(starts_with("Sepal"), quantile_df, .unpack = "{outer}.{inner}"))
#' reframe(
#' across(starts_with("Sepal"), quantile_df, .unpack = "{outer}.{inner}")
#' )
#'
#' # This is also useful inside mutate(), for example, with a multi-lag helper
#' multilag <- function(x, lags = 1:3) {
Expand Down Expand Up @@ -618,9 +637,11 @@ expand_if_across <- function(quo) {
if (is_call(call, "if_any")) {
op <- "|"
if_fn <- "if_any"
empty <- FALSE
} else {
op <- "&"
if_fn <- "if_all"
empty <- TRUE
}

context_local("across_if_fn", if_fn)
Expand All @@ -634,9 +655,10 @@ expand_if_across <- function(quo) {
call[[1]] <- quote(across)
quos <- expand_across(quo_set_expr(quo, call))

# Select all rows if there are no inputs
# Select all rows if there are no inputs for if_all(),
# but select no rows if there are no inputs for if_any().
if (!length(quos)) {
return(list(quo(TRUE)))
return(list(quo(!!empty)))
}

combine <- function(x, y) {
Expand Down
26 changes: 23 additions & 3 deletions man/across.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion tests/testthat/test-across.R
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ test_that("if_any() and if_all() expansions deal with no inputs or single inputs
# No inputs
expect_equal(
filter(d, if_any(starts_with("c"), ~ FALSE)),
filter(d)
filter(d, FALSE)
)
expect_equal(
filter(d, if_all(starts_with("c"), ~ FALSE)),
Expand All @@ -888,6 +888,32 @@ test_that("if_any() and if_all() expansions deal with no inputs or single inputs
)
})

test_that("if_any() on zero-column selection behaves like any() (#7059)", {
tbl <- tibble(
x1 = 1:5,
x2 = c(-1, 4, 5, 4, 1),
y = c(1, 4, 2, 4, 9),
)

expect_equal(
filter(tbl, if_any(c(), ~ is.na(.x))),
tbl[0, ]
)
})

test_that("if_all() on zero-column selection behaves like all() (#7059)", {
tbl <- tibble(
x1 = 1:5,
x2 = c(-1, 4, 5, 4, 1),
y = c(1, 4, 2, 4, 9),
)

expect_equal(
filter(tbl, if_all(c(), ~ is.na(.x))),
tbl
)
})

test_that("if_any() and if_all() wrapped deal with no inputs or single inputs", {
d <- data.frame(x = 1)

Expand Down

0 comments on commit b2c7e04

Please sign in to comment.