Skip to content

Commit 189203c

Browse files
committed
Add handling of one-sided formulae
1 parent 13ee660 commit 189203c

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

r/R/dplyr-funcs-conditional.R

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ parse_condition_formulas <- function(formulas, mask, fn) {
3838
# Process each formula: condition ~ value
3939
for (i in seq_len(n)) {
4040
f <- formulas[[i]]
41-
if (!inherits(f, "formula")) {
41+
if (!is_formula(f, lhs = TRUE)) {
4242
validation_error(paste0("Each argument to ", fn, "() must be a two-sided formula"))
4343
}
4444
# f[[2]] is LHS (logical condition), f[[3]] is RHS (value when TRUE)
@@ -95,7 +95,17 @@ parse_from_to_mapping <- function(x, from, to) {
9595
query[[i]] <- call_binding("is.na", x)
9696
} else if (length(from_i) > 1) {
9797
# Multiple values: use %in% to match any
98-
query[[i]] <- call_binding("%in%", x, from_i)
98+
# If NA is in the vector, also match NA using is.na()
99+
if (any(is.na(from_i))) {
100+
non_na_values <- from_i[!is.na(from_i)]
101+
if (length(non_na_values) > 0) {
102+
query[[i]] <- call_binding("%in%", x, non_na_values) | call_binding("is.na", x)
103+
} else {
104+
query[[i]] <- call_binding("is.na", x)
105+
}
106+
} else {
107+
query[[i]] <- call_binding("%in%", x, from_i)
108+
}
99109
} else {
100110
query[[i]] <- x == from_i
101111
}
@@ -123,7 +133,7 @@ parse_formula_mapping <- function(x, formulas, mask, fn) {
123133
value <- vector("list", n)
124134
for (i in seq_len(n)) {
125135
f <- formulas[[i]]
126-
if (!inherits(f, "formula")) {
136+
if (!is_formula(f, lhs = TRUE)) {
127137
validation_error(paste0("Each argument to ", fn, "() must be a two-sided formula"))
128138
}
129139
# f[[2]] is LHS (value to match), f[[3]] is RHS (replacement)
@@ -138,7 +148,17 @@ parse_formula_mapping <- function(x, formulas, mask, fn) {
138148
query[[i]] <- call_binding("is.na", x)
139149
} else if (!inherits(lhs, "Expression") && length(lhs) > 1) {
140150
# Vector LHS: c("a", "b") ~ "X" matches any value in the vector
141-
query[[i]] <- call_binding("%in%", x, lhs)
151+
# If NA is in the vector, also match NA using is.na()
152+
if (any(is.na(lhs))) {
153+
non_na_values <- lhs[!is.na(lhs)]
154+
if (length(non_na_values) > 0) {
155+
query[[i]] <- call_binding("%in%", x, non_na_values) | call_binding("is.na", x)
156+
} else {
157+
query[[i]] <- call_binding("is.na", x)
158+
}
159+
} else {
160+
query[[i]] <- call_binding("%in%", x, lhs)
161+
}
142162
} else {
143163
query[[i]] <- x == lhs
144164
}

r/tests/testthat/test-dplyr-funcs-conditional.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,11 @@ test_that("replace_when()", {
557557
"Each argument to replace_when\\(\\) must be a two-sided formula",
558558
class = "validation_error"
559559
)
560+
expect_arrow_eval_error(
561+
replace_when(int, ~ 100L),
562+
"Each argument to replace_when\\(\\) must be a two-sided formula",
563+
class = "validation_error"
564+
)
560565
expect_arrow_eval_error(
561566
replace_when(int, 0L ~ 100L),
562567
"Left side of each formula in replace_when\\(\\) must be a logical expression",
@@ -629,6 +634,22 @@ test_that("replace_values()", {
629634
tbl
630635
)
631636

637+
# multiple values on LHS including NA matches any including NA
638+
compare_dplyr_binding(
639+
.input |>
640+
mutate(result = replace_values(chr, c(NA, "a") ~ "matched")) |>
641+
collect(),
642+
tbl
643+
)
644+
645+
# from/to with list containing NA matches NA too
646+
compare_dplyr_binding(
647+
.input |>
648+
mutate(result = replace_values(chr, from = list(c(NA, "a"), "b"), to = c("matched", "B"))) |>
649+
collect(),
650+
tbl
651+
)
652+
632653
# no replacements returns x unchanged
633654
compare_dplyr_binding(
634655
.input |>
@@ -643,6 +664,11 @@ test_that("replace_values()", {
643664
"Each argument to replace_values\\(\\) must be a two-sided formula",
644665
class = "validation_error"
645666
)
667+
expect_arrow_eval_error(
668+
replace_values(chr, ~ "A"),
669+
"Each argument to replace_values\\(\\) must be a two-sided formula",
670+
class = "validation_error"
671+
)
646672
expect_arrow_eval_error(
647673
replace_values(chr, "a" ~ "A", from = "b"),
648674
"Can't use both `...` and `from`/`to` in replace_values\\(\\)",
@@ -723,6 +749,11 @@ test_that("recode_values()", {
723749
"Each argument to recode_values\\(\\) must be a two-sided formula",
724750
class = "validation_error"
725751
)
752+
expect_arrow_eval_error(
753+
recode_values(chr, ~ "A"),
754+
"Each argument to recode_values\\(\\) must be a two-sided formula",
755+
class = "validation_error"
756+
)
726757
expect_arrow_eval_error(
727758
recode_values(chr, "a" ~ "A", from = "b"),
728759
"Can't use both `...` and `from`/`to` in recode_values\\(\\)",

0 commit comments

Comments
 (0)