Skip to content

Commit 622afe8

Browse files
Add .by to fill() (#1572)
* Add .by to `fill()` * Add news * Add it back to the generic * Add extra validation when `.by` is supplied and a group data frame (repeat the error message from mutate() for a better stack trace. * Move `.by` to the data frame method, remove `.by` handling, misc tweaks * Go back to `.by` in the generic of `fill()` --------- Co-authored-by: Davis Vaughan <[email protected]>
1 parent 454a4c8 commit 622afe8

File tree

5 files changed

+118
-18
lines changed

5 files changed

+118
-18
lines changed

NEWS.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# tidyr (development version)
22

3+
* `fill()` gains a `.by` argument as an alternative to `dplyr::group_by()` for
4+
applying the fill per group, similar to `nest(.by =)` and
5+
`dplyr::mutate(.by =)` (@olivroy, #1439).
6+
37
* `unchop()` produces a more helpful error message when columns cannot be cast
48
to `ptype` (@mgirlich, #1477).
59

R/fill.R

+31-9
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
#' @section Grouped data frames:
1010
#' With grouped data frames created by [dplyr::group_by()], `fill()` will be
1111
#' applied _within_ each group, meaning that it won't fill across group
12-
#' boundaries.
12+
#' boundaries. This can also be accomplished using the `.by` argument to
13+
#' `fill()`, which creates a temporary grouping for just this operation.
14+
#'
15+
#' @inheritParams dplyr::mutate
1316
#'
1417
#' @param data A data frame.
1518
#' @param ... <[`tidy-select`][tidyr_tidy_select]> Columns to fill.
1619
#' @param .direction Direction in which to fill missing values. Currently
1720
#' either "down" (the default), "up", "downup" (i.e. first down and then up)
1821
#' or "updown" (first up and then down).
22+
#'
1923
#' @export
2024
#' @examples
2125
#' # direction = "down" --------------------------------------------------------
@@ -82,22 +86,36 @@
8286
#' 3, "Danielle", "Observer", NA
8387
#' )
8488
#'
85-
#' # The values are inconsistently missing by position within the group
86-
#' # Use .direction = "downup" to fill missing values in both directions
89+
#' # The values are inconsistently missing by position within the `group`.
90+
#' # Use `.direction = "downup"` to fill missing values in both directions
91+
#' # and `.by = group` to apply the fill per group.
92+
#' squirrels %>%
93+
#' fill(n_squirrels, .direction = "downup", .by = group)
94+
#'
95+
#' # If you want, you can also supply a data frame grouped with `group_by()`,
96+
#' # but don't forget to `ungroup()`!
8797
#' squirrels %>%
8898
#' dplyr::group_by(group) %>%
8999
#' fill(n_squirrels, .direction = "downup") %>%
90100
#' dplyr::ungroup()
91-
#'
92-
#' # Using `.direction = "updown"` accomplishes the same goal in this example
93-
fill <- function(data, ..., .direction = c("down", "up", "downup", "updown")) {
101+
fill <- function(data,
102+
...,
103+
.by = NULL,
104+
.direction = c("down", "up", "downup", "updown")) {
94105
check_dots_unnamed()
95106
UseMethod("fill")
96107
}
97108

98109
#' @export
99-
fill.data.frame <- function(data, ..., .direction = c("down", "up", "downup", "updown")) {
100-
vars <- tidyselect::eval_select(expr(c(...)), data, allow_rename = FALSE)
110+
fill.data.frame <- function(data,
111+
...,
112+
.by = NULL,
113+
.direction = c("down", "up", "downup", "updown")) {
114+
vars <- names(tidyselect::eval_select(
115+
expr = expr(c(...)),
116+
data = data,
117+
allow_rename = FALSE
118+
))
101119

102120
.direction <- arg_match0(
103121
arg = .direction,
@@ -108,5 +126,9 @@ fill.data.frame <- function(data, ..., .direction = c("down", "up", "downup", "u
108126
vec_fill_missing(col, direction = .direction)
109127
}
110128

111-
dplyr::mutate_at(data, .vars = dplyr::vars(any_of(vars)), .funs = fn)
129+
dplyr::mutate(
130+
data,
131+
dplyr::across(any_of(vars), .fns = fn),
132+
.by = {{ .by }}
133+
)
112134
}

man/fill.Rd

+17-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/fill.md

+27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# errors on named `...` inputs
2+
3+
Code
4+
fill(df, fooy = x)
5+
Condition
6+
Error in `fill()`:
7+
! Arguments in `...` must be passed by position, not name.
8+
x Problematic argument:
9+
* fooy = x
10+
111
# validates its inputs
212

313
Code
@@ -6,3 +16,20 @@
616
Error in `fill()`:
717
! `.direction` must be one of "down", "up", "downup", or "updown", not "foo".
818

19+
# `.by` can't select columns that don't exist
20+
21+
Code
22+
fill(df, y, .by = z)
23+
Condition
24+
Error in `dplyr::mutate()`:
25+
! Can't select columns that don't exist.
26+
x Column `z` doesn't exist.
27+
28+
# `.by` can't be used on a grouped data frame
29+
30+
Code
31+
fill(df, y, .by = x)
32+
Condition
33+
Error in `dplyr::mutate()`:
34+
! Can't supply `.by` when `.data` is a grouped data frame.
35+

tests/testthat/test-fill.R

+39-3
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,59 @@ test_that("fill preserves attributes", {
106106
expect_equal(attributes(out_u$x), attributes(df$x))
107107
})
108108

109-
test_that("fill respects grouping", {
109+
test_that("fill respects existing grouping and `.by`", {
110110
df <- tibble(x = c(1, 1, 2), y = c(1, NA, NA))
111+
111112
out <- df %>%
112113
dplyr::group_by(x) %>%
113114
fill(y)
114-
expect_equal(out$y, c(1, 1, NA))
115+
expect_identical(out$y, c(1, 1, NA))
116+
expect_identical(dplyr::group_vars(out), "x")
117+
118+
out <- df %>%
119+
fill(y, .by = x)
120+
expect_identical(out$y, c(1, 1, NA))
121+
expect_identical(dplyr::group_vars(out), character())
115122
})
116123

117124
test_that("works when there is a column named `.direction` in the data (#1319)", {
118125
df <- tibble(x = c(1, NA, 2), .direction = 1:3)
119-
expect_error(out <- fill(df, x), NA)
126+
out <- fill(df, x)
120127
expect_identical(out$x, c(1, 1, 2))
121128
})
122129

130+
test_that("errors on named `...` inputs", {
131+
df <- tibble(x = c(1, NA, 2))
132+
133+
expect_snapshot(error = TRUE, {
134+
fill(df, fooy = x)
135+
})
136+
})
137+
123138
test_that("validates its inputs", {
124139
df <- tibble(x = c(1, NA, 2))
125140
expect_snapshot(error = TRUE, {
126141
df %>% fill(x, .direction = "foo")
127142
})
128143
})
144+
145+
test_that("`.by` can't select columns that don't exist", {
146+
df <- tibble(x = 1, y = 2)
147+
148+
# This shows `mutate()` in the error, but we accept that to not have to handle
149+
# `.by` in any way.
150+
expect_snapshot(error = TRUE, {
151+
fill(df, y, .by = z)
152+
})
153+
})
154+
155+
test_that("`.by` can't be used on a grouped data frame", {
156+
df <- tibble(x = 1, y = 2)
157+
df <- dplyr::group_by(df, x)
158+
159+
# This shows `mutate()` in the error, but we accept that to not have to handle
160+
# `.by` in any way.
161+
expect_snapshot(error = TRUE, {
162+
fill(df, y, .by = x)
163+
})
164+
})

0 commit comments

Comments
 (0)