From 931d4ec971a6c002c7c6c537e91713ed608f44b6 Mon Sep 17 00:00:00 2001 From: Joe Thorley Date: Tue, 8 Sep 2026 12:15:41 -0700 Subject: [PATCH] Error informatively when xobs_only() arguments are named or unknown A named argument inside xobs_only() failed with an internal stopifnot from xcast() and a dplyr cross-join deprecation warning. Names are now rejected up front, since a new column has no observed combinations to preserve, and semi_crossing() errors when an argument does not refer to a column of .data instead of falling through to an empty join. Closes #111. Co-Authored-By: Claude Fable 5.1 --- R/xobs-only.R | 25 +++++++++++++++++++++-- man/xobs_only.Rd | 5 ++++- tests/testthat/_snaps/xobs-only.md | 23 +++++++++++++++++++++ tests/testthat/test-xobs-only.R | 32 ++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/_snaps/xobs-only.md create mode 100644 tests/testthat/test-xobs-only.R diff --git a/R/xobs-only.R b/R/xobs-only.R index db7d620..09457a5 100644 --- a/R/xobs-only.R +++ b/R/xobs-only.R @@ -1,6 +1,9 @@ #' Generate Observed Combinations Only #' -#' @param ... One or more variables to generate combinations for. +#' @param ... One or more unnamed variables in `.data` to generate observed +#' combinations for. Naming an argument is an error as a new column has no +#' observed combinations to preserve; use a named argument to [xnew_data()] +#' instead. #' @param .length_out A count to override the default length of sequences. #' @inheritParams xcast #' @return A tibble of the observed combinations of the variables. @@ -19,6 +22,15 @@ xobs_only <- function(..., .length_out = NULL, .data = xnew_data_env$data) { quos <- enquos(...) + named <- names2(quos)[nzchar(names2(quos))] + if (length(named)) { + err( + "`xobs_only()` arguments must not be named (", + cc(named, " and "), + ") as observed combinations must refer to columns of `.data`" + ) + } + translated <- map(quos, quo_translate_xobs_only, .length_out) out <- semi_crossing(!!!translated) @@ -46,5 +58,14 @@ semi_crossing <- function(..., .data = xnew_data_env$data) { } out <- tidyr::crossing(...) - dplyr::semi_join(out, .data, by = intersect(names(out), names(.data))) + + missing <- setdiff(names(out), names(.data)) + if (length(missing)) { + err( + "`xobs_only()` arguments must refer to columns of `.data` (unrecognised: ", + cc(missing, " and "), + ")" + ) + } + dplyr::semi_join(out, .data, by = names(out)) } diff --git a/man/xobs_only.Rd b/man/xobs_only.Rd index 068996c..747548e 100644 --- a/man/xobs_only.Rd +++ b/man/xobs_only.Rd @@ -7,7 +7,10 @@ xobs_only(..., .length_out = NULL, .data = xnew_data_env$data) } \arguments{ -\item{...}{One or more variables to generate combinations for.} +\item{...}{One or more unnamed variables in \code{.data} to generate observed +combinations for. Naming an argument is an error as a new column has no +observed combinations to preserve; use a named argument to \code{\link[=xnew_data]{xnew_data()}} +instead.} \item{.length_out}{A count to override the default length of sequences.} diff --git a/tests/testthat/_snaps/xobs-only.md b/tests/testthat/_snaps/xobs-only.md new file mode 100644 index 0000000..0b40893 --- /dev/null +++ b/tests/testthat/_snaps/xobs-only.md @@ -0,0 +1,23 @@ +# xobs_only errors informatively for named and unknown arguments (#111) + + Code + xnew_data(data, xobs_only(z = b)) + Condition + Error: + ! `xobs_only()` arguments must not be named ('z') as observed combinations must refer to columns of `.data`. + Code + xnew_data(data, xobs_only(a, z = new_seq(b, .length_out = 2))) + Condition + Error: + ! `xobs_only()` arguments must not be named ('z') as observed combinations must refer to columns of `.data`. + Code + xnew_data(data, xobs_only(b = b)) + Condition + Error: + ! `xobs_only()` arguments must not be named ('b') as observed combinations must refer to columns of `.data`. + Code + xnew_data(data, xobs_only(new_seq(b, .length_out = 2))) + Condition + Error: + ! `xobs_only()` arguments must refer to columns of `.data` (unrecognised: 'new_seq(b, .length_out = 2)'). + diff --git a/tests/testthat/test-xobs-only.R b/tests/testthat/test-xobs-only.R new file mode 100644 index 0000000..f6c8b6a --- /dev/null +++ b/tests/testthat/test-xobs-only.R @@ -0,0 +1,32 @@ +test_that("xobs_only preserves observed combinations", { + data <- tibble::tibble( + a = c(1.5, 2.5, 3.5), + b = factor(c("a", "a", "b"), levels = c("a", "b", "c")) + ) + + new_data <- xnew_data(data, xobs_only(a, b)) + expect_identical(new_data$a, data$a) + expect_identical(new_data$b, data$b) + + new_data <- xnew_data(data, xobs_only(a, xnew_seq(b, .length_out = 1))) + expect_identical(new_data$a, c(1.5, 2.5)) + expect_identical(new_data$b, factor(c("a", "a"), levels = c("a", "b", "c"))) +}) + +test_that("xobs_only errors informatively for named and unknown arguments (#111)", { + data <- tibble::tibble( + a = 1:5 + 0.5, + b = factor(letters[1:5]) + ) + + expect_snapshot(error = TRUE, { + xnew_data(data, xobs_only(z = b)) + xnew_data(data, xobs_only(a, z = new_seq(b, .length_out = 2))) + xnew_data(data, xobs_only(b = b)) + xnew_data(data, xobs_only(new_seq(b, .length_out = 2))) + }) + + # naming the whole xobs_only() call still creates a new column + new_data <- xnew_data(data, z = xobs_only(b)) + expect_named(new_data, c("a", "b", "z")) +})