Skip to content

Commit c8482ba

Browse files
committed
Implement vec_case_when()
1 parent 2ab2cf4 commit c8482ba

File tree

15 files changed

+1624
-0
lines changed

15 files changed

+1624
-0
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ export(vec_as_subscript2)
540540
export(vec_assert)
541541
export(vec_assign)
542542
export(vec_c)
543+
export(vec_case_when)
543544
export(vec_cast)
544545
export(vec_cast.Date)
545546
export(vec_cast.POSIXct)
@@ -632,6 +633,7 @@ export(vec_recycle_common)
632633
export(vec_rep)
633634
export(vec_rep_each)
634635
export(vec_repeat)
636+
export(vec_replace_when)
635637
export(vec_restore)
636638
export(vec_run_sizes)
637639
export(vec_seq_along)

NEWS.md

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

3+
* New `vec_case_when()` and `vec_replace_when()` for recoding and replacing using logical conditions (#2024).
4+
35
* `vec_equal()` now efficiently internally recycles `x` and `y` elements of size 1 (#2028).
46

57
* `list_unchop()` now assigns names correctly when overlapping `indices` are involved (#2019).

R/case-when.R

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#' Recode and replace using logical conditions
2+
#'
3+
#' @description
4+
#'
5+
#' - `vec_case_when()` constructs an entirely new vector by recoding the `TRUE`
6+
#' `cases` to their corresponding `values`. If there are locations not matched
7+
#' by `cases`, then they are recoded to the `default` value.
8+
#'
9+
#' - `vec_replace_when()` updates an existing vector by replacing the values
10+
#' from `x` matched by the `TRUE` `cases` with their corresponding `values`.
11+
#' In this case, each element of `values` must have the same type as `x` and
12+
#' locations not matched by `cases` retain their original `x` value.
13+
#'
14+
#' `vec_case_when()` is often thought of as a way to vectorize multiple if-else
15+
#' statements, and is an R equivalent of the SQL "searched" `CASE WHEN`
16+
#' statement.
17+
#'
18+
#' @inheritParams rlang::args_dots_empty
19+
#' @inheritParams rlang::args_error_context
20+
#'
21+
#' @param x A vector.
22+
#'
23+
#' @param cases A list of logical vectors.
24+
#'
25+
#' For `vec_case_when()`, each vector should be the same size.
26+
#'
27+
#' For `vec_replace_when()`, each vector should be the same size as `x`.
28+
#'
29+
#' Where a value in `cases` is `TRUE`, the corresponding value in `values`
30+
#' will be assigned to the result.
31+
#'
32+
#' @param values A list of vectors.
33+
#'
34+
#' For `vec_case_when()`, each vector should be size 1 or the size implied by
35+
#' `cases`. The common type of `values` and `default` determine the output
36+
#' type, unless overridden by `ptype`.
37+
#'
38+
#' For `vec_replace_when()`, each vector should be size 1 or the same size
39+
#' as `x`. Each vector will be cast to the type of `x`.
40+
#'
41+
#' @param default Default value to use when `cases` does not match every
42+
#' location in the output.
43+
#'
44+
#' By default, a missing value is used as the default value.
45+
#'
46+
#' If supplied, `default` must be size 1 or the size implied by `cases`.
47+
#'
48+
#' Can only be set when `unmatched = "default"`.
49+
#'
50+
#' @param unmatched Handling of unmatched locations.
51+
#'
52+
#' One of:
53+
#'
54+
#' - `"default"` to use `default` in unmatched locations.
55+
#'
56+
#' - `"error"` to error when there are unmatched locations.
57+
#'
58+
#' @param ptype An optional override for the output type, which is usually
59+
#' computed as the common type of `values` and `default`.
60+
#'
61+
#' @param size An optional override for the output size, which is usually
62+
#' computed as the size of the first element of `cases`.
63+
#'
64+
#' Only useful for requiring a fixed size when `cases` is an empty list.
65+
#'
66+
#' @param x_arg,cases_arg,values_arg,default_arg Argument names used in error
67+
#' messages.
68+
#'
69+
#' @returns
70+
#' A vector.
71+
#'
72+
#' - For `vec_case_when()`, the type of the output is computed as the common
73+
#' type of `values` and `default`, unless overridden by `ptype`. The names of
74+
#' the output come from the names of `values` and `default`. The size of the
75+
#' output comes from the implied size from `cases`, unless overridden by
76+
#' `size`.
77+
#'
78+
#' - For `vec_replace_when()`, the type of the output will have the same type as
79+
#' `x`. The names of the output will be the same as the names of `x`. The size
80+
#' of the output will be the same size as `x`.
81+
#'
82+
#' @name vec-case-and-replace
83+
#'
84+
#' @examples
85+
#' # Note how the first `TRUE` is used in the output.
86+
#' # Also note how the `NA` falls through to `default`.
87+
#' x <- seq(-2L, 2L, by = 1L)
88+
#' x <- c(x, NA)
89+
#' cases <- list(
90+
#' x < 0,
91+
#' x < 1
92+
#' )
93+
#' values <- list(
94+
#' "<0",
95+
#' "<1"
96+
#' )
97+
#' vec_case_when(
98+
#' cases,
99+
#' values,
100+
#' default = "other"
101+
#' )
102+
#'
103+
#' # Missing values need to be handled with their own case
104+
#' # if you want them to have a special value
105+
#' cases <- list(
106+
#' x < 0,
107+
#' x < 1,
108+
#' is.na(x)
109+
#' )
110+
#' values <- list(
111+
#' "<0",
112+
#' "<1",
113+
#' NA
114+
#' )
115+
#' vec_case_when(
116+
#' cases,
117+
#' values,
118+
#' default = "other"
119+
#' )
120+
#'
121+
#' # Both `values` and `default` are vectorized
122+
#' values <- list(
123+
#' x * 5,
124+
#' x * 10,
125+
#' NA
126+
#' )
127+
#' vec_case_when(
128+
#' cases,
129+
#' values,
130+
#' default = x * 100
131+
#' )
132+
#'
133+
#' # Use `vec_replace_when()` if you need to update `x`, retaining
134+
#' # all previous values in locations that you don't match
135+
#' cases <- list(
136+
#' x < 0,
137+
#' x < 1
138+
#' )
139+
#' values <- list(
140+
#' 0,
141+
#' 1
142+
#' )
143+
#' out <- vec_replace_when(
144+
#' x,
145+
#' cases,
146+
#' values
147+
#' )
148+
#' out
149+
#'
150+
#' # Note how `vec_replace_when()` is type stable on `x`, we retain the
151+
#' # integer type here even though `values` contained doubles
152+
#' typeof(out)
153+
#'
154+
#' # `vec_case_when()` creates a new vector, so names come from `values`
155+
#' # and `default`. `vec_replace_when()` modifies an existing vector, so
156+
#' # names come from `x` no matter what, just like `[<-` and `base::replace()`
157+
#' x <- c(a = 1, b = 2, c = 3)
158+
#' cases <- list(x == 1, x == 2)
159+
#' values <- list(c(x = 0), c(y = -1))
160+
#' vec_case_when(cases, values)
161+
#' vec_replace_when(x, cases, values)
162+
#'
163+
#' # If you want to enforce that you've covered all of the locations in your
164+
#' # `cases`, use `unmatched = "error"` rather than providing a `default`
165+
#' x <- c(0, 1, 2)
166+
#' cases <- list(x == 1, x == 2)
167+
#' values <- list("a", "b")
168+
#' try(vec_case_when(cases, values, unmatched = "error"))
169+
NULL
170+
171+
#' @rdname vec-case-and-replace
172+
#' @export
173+
vec_case_when <- function(
174+
cases,
175+
values,
176+
...,
177+
default = NULL,
178+
unmatched = "default",
179+
ptype = NULL,
180+
size = NULL,
181+
cases_arg = "cases",
182+
values_arg = "values",
183+
default_arg = "default",
184+
error_call = current_env()
185+
) {
186+
check_dots_empty0(...)
187+
.Call(
188+
ffi_vec_case_when,
189+
cases,
190+
values,
191+
default,
192+
unmatched,
193+
ptype,
194+
size,
195+
environment()
196+
)
197+
}
198+
199+
#' @rdname vec-case-and-replace
200+
#' @export
201+
vec_replace_when <- function(
202+
x,
203+
cases,
204+
values,
205+
...,
206+
x_arg = "x",
207+
cases_arg = "cases",
208+
values_arg = "values",
209+
error_call = current_env()
210+
) {
211+
check_dots_empty0(...)
212+
.Call(
213+
ffi_vec_replace_when,
214+
x,
215+
cases,
216+
values,
217+
environment()
218+
)
219+
}

_pkgdown.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ reference:
6464
- vec_assign
6565
- vec_fill_missing
6666

67+
- title: Recoding
68+
contents:
69+
- vec_case_when
70+
6771
- title: Equality and ordering
6872
contents:
6973
- vec_equal

0 commit comments

Comments
 (0)