Skip to content

Commit 8bdbb39

Browse files
authored
Add .unmatched to case_when() (#7737)
* Add `.unmatched` to `case_when()` * Regenerate snapshots with r-lib/vctrs#2063
1 parent 345a1dc commit 8bdbb39

File tree

5 files changed

+250
-2
lines changed

5 files changed

+250
-2
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# dplyr (development version)
22

3+
* `case_when()` has gained a new `.unmatched` argument. For extra safety, set `.unmatched = "error"` rather than providing a `.default` when you believe that you've handled every possible case, and it will error if a case is left unhandled. The new `recode_values()` also has this argument (#7653).
4+
35
* New `rbind()` method for `rowwise_df` to avoid creating corrupt rowwise data frames (r-lib/vctrs#1935).
46

57
* `case_match()` is now superseded by `recode_values()` and `replace_values()`.

R/case-when.R

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
#' `.default`. This typically involves some variation of `is.na(x) ~ value`
6161
#' tailored to your usage of `case_when()`.
6262
#'
63+
#' @param .unmatched Handling of unmatched locations.
64+
#'
65+
#' One of:
66+
#'
67+
#' - `"default"` to use `.default` in unmatched locations.
68+
#'
69+
#' - `"error"` to error when there are unmatched locations.
70+
#'
6371
#' @param .ptype An optional prototype declaring the desired output type. If
6472
#' supplied, this overrides the common type of the RHS inputs.
6573
#'
@@ -116,6 +124,46 @@
116124
#' .default = as.character(x)
117125
#' )
118126
#'
127+
#' # If you believe that you've covered every possible case, then set
128+
#' # `.unmatched = "error"` rather than supplying a `.default`. This adds an
129+
#' # extra layer of safety to `case_when()` and is particularly useful when you
130+
#' # have a series of complex expressions!
131+
#' set.seed(123)
132+
#' x <- sample(50)
133+
#'
134+
#' # Oops, we forgot to handle `50`
135+
#' try(case_when(
136+
#' x < 10 ~ "ten",
137+
#' x < 20 ~ "twenty",
138+
#' x < 30 ~ "thirty",
139+
#' x < 40 ~ "forty",
140+
#' x < 50 ~ "fifty",
141+
#' .unmatched = "error"
142+
#' ))
143+
#'
144+
#' case_when(
145+
#' x < 10 ~ "ten",
146+
#' x < 20 ~ "twenty",
147+
#' x < 30 ~ "thirty",
148+
#' x < 40 ~ "forty",
149+
#' x <= 50 ~ "fifty",
150+
#' .unmatched = "error"
151+
#' )
152+
#'
153+
#' # Note that `NA` is considered unmatched and must be handled with its own
154+
#' # explicit case, even if that case just propagates the missing value!
155+
#' x[c(2, 5)] <- NA
156+
#'
157+
#' case_when(
158+
#' x < 10 ~ "ten",
159+
#' x < 20 ~ "twenty",
160+
#' x < 30 ~ "thirty",
161+
#' x < 40 ~ "forty",
162+
#' x <= 50 ~ "fifty",
163+
#' is.na(x) ~ NA,
164+
#' .unmatched = "error"
165+
#' )
166+
#'
119167
#' # `replace_when()` is useful when you're updating an existing vector,
120168
#' # rather than creating an entirely new one. Note the so-far unused "puppy"
121169
#' # factor level:
@@ -216,7 +264,13 @@ NULL
216264

217265
#' @rdname case-and-replace-when
218266
#' @export
219-
case_when <- function(..., .default = NULL, .ptype = NULL, .size = NULL) {
267+
case_when <- function(
268+
...,
269+
.default = NULL,
270+
.unmatched = "default",
271+
.ptype = NULL,
272+
.size = NULL
273+
) {
220274
args <- eval_formulas(..., allow_empty_dots = FALSE)
221275
conditions <- args$lhs
222276
values <- args$rhs
@@ -236,6 +290,7 @@ case_when <- function(..., .default = NULL, .ptype = NULL, .size = NULL) {
236290
conditions = conditions,
237291
values = values,
238292
default = .default,
293+
unmatched = .unmatched,
239294
ptype = .ptype,
240295
size = .size,
241296
conditions_arg = "",

man/case-and-replace-when.Rd

Lines changed: 55 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/case-when.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,75 @@
6262
Error in `case_when()`:
6363
! Can't recycle `..1 (right)` (size 2) to size 3.
6464

65+
# can't supply `.default` and `.unmatched`
66+
67+
Code
68+
case_when(TRUE ~ 1, .default = 1, .unmatched = "error")
69+
Condition
70+
Error in `case_when()`:
71+
! Can't set `.default` when `unmatched = "error"`.
72+
73+
# `.unmatched` is validated
74+
75+
Code
76+
case_when(TRUE ~ 1, .unmatched = "foo")
77+
Condition
78+
Error in `case_when()`:
79+
! `unmatched` must be either "default" or "error", not "foo".
80+
81+
---
82+
83+
Code
84+
case_when(TRUE ~ 1, .unmatched = 1)
85+
Condition
86+
Error in `case_when()`:
87+
! `unmatched` must be a string, not the number 1.
88+
89+
# `.unmatched` treats `FALSE` like an unmatched location
90+
91+
Code
92+
case_when(c(TRUE, FALSE, TRUE) ~ 1, .unmatched = "error")
93+
Condition
94+
Error in `case_when()`:
95+
! Each location must be matched.
96+
x Location 2 is unmatched.
97+
98+
# `.unmatched` treats `NA` like an unmatched location
99+
100+
Code
101+
case_when(c(TRUE, NA, TRUE) ~ 1, .unmatched = "error")
102+
Condition
103+
Error in `case_when()`:
104+
! Each location must be matched.
105+
x Location 2 is unmatched.
106+
107+
# `.unmatched` errors pluralize well
108+
109+
Code
110+
case_when(x == "a" ~ 1, x == "b" ~ 2, x == "c" ~ 3, x == "e" ~ 4, .unmatched = "error")
111+
Condition
112+
Error in `case_when()`:
113+
! Each location must be matched.
114+
x Location 4 is unmatched.
115+
116+
---
117+
118+
Code
119+
case_when(x == "a" ~ 1, x == "c" ~ 2, x == "e" ~ 3, .unmatched = "error")
120+
Condition
121+
Error in `case_when()`:
122+
! Each location must be matched.
123+
x Locations 2 and 4 are unmatched.
124+
125+
---
126+
127+
Code
128+
case_when(x == 1 ~ "a", .unmatched = "error")
129+
Condition
130+
Error in `case_when()`:
131+
! Each location must be matched.
132+
x Locations 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ..., 99, and 100 are unmatched.
133+
65134
# invalid type errors are correct (#6261) (#6206)
66135

67136
Code

tests/testthat/test-case-when.R

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,74 @@ test_that("passes through `.size` correctly", {
247247
})
248248
})
249249

250+
test_that("can't supply `.default` and `.unmatched`", {
251+
# Probably overkill to add `unmatched_arg` just to get `.unmatched` instead
252+
# of `unmatched`.
253+
expect_snapshot(error = TRUE, {
254+
case_when(TRUE ~ 1, .default = 1, .unmatched = "error")
255+
})
256+
})
257+
258+
test_that("`.unmatched` is validated", {
259+
# Probably overkill to add `unmatched_arg` to `vec_case_when()` just to get
260+
# `.unmatched` instead of `unmatched`
261+
expect_snapshot(error = TRUE, {
262+
case_when(TRUE ~ 1, .unmatched = "foo")
263+
})
264+
expect_snapshot(error = TRUE, {
265+
case_when(TRUE ~ 1, .unmatched = 1)
266+
})
267+
})
268+
269+
test_that("`.unmatched` treats `FALSE` like an unmatched location", {
270+
expect_snapshot(error = TRUE, {
271+
case_when(
272+
c(TRUE, FALSE, TRUE) ~ 1,
273+
.unmatched = "error"
274+
)
275+
})
276+
})
277+
278+
test_that("`.unmatched` treats `NA` like an unmatched location", {
279+
expect_snapshot(error = TRUE, {
280+
case_when(
281+
c(TRUE, NA, TRUE) ~ 1,
282+
.unmatched = "error"
283+
)
284+
})
285+
})
286+
287+
test_that("`.unmatched` errors pluralize well", {
288+
# One location
289+
x <- letters[1:5]
290+
expect_snapshot(error = TRUE, {
291+
case_when(
292+
x == "a" ~ 1,
293+
x == "b" ~ 2,
294+
x == "c" ~ 3,
295+
x == "e" ~ 4,
296+
.unmatched = "error"
297+
)
298+
})
299+
300+
# Two locations
301+
x <- letters[1:5]
302+
expect_snapshot(error = TRUE, {
303+
case_when(
304+
x == "a" ~ 1,
305+
x == "c" ~ 2,
306+
x == "e" ~ 3,
307+
.unmatched = "error"
308+
)
309+
})
310+
311+
# Many locations
312+
x <- 1:100
313+
expect_snapshot(error = TRUE, {
314+
case_when(x == 1 ~ "a", .unmatched = "error")
315+
})
316+
})
317+
250318
# `case_when()` errors ---------------------------------------------------------
251319

252320
test_that("invalid type errors are correct (#6261) (#6206)", {

0 commit comments

Comments
 (0)