diff --git a/NEWS.md b/NEWS.md index 903031d296..16cfc2f680 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # dplyr (development version) +* `between()` gains a new `ptype` argument, allowing users to specify the + desired output type. This is particularly useful for ordered factors and other + complex types where the default common type behavior might not be ideal + (#6906, @JamesHWade). + * Fixed an edge case when coercing data frames to matrices (#7004). * Fixed an issue where duckplyr's ALTREP data frames were being materialized @@ -7,8 +12,8 @@ * R >=3.6.0 is now explicitly required (#7026). -* `if_any()` and `if_all()` are now fully consistent with `any()` and `all()`. - In particular, when called with empty inputs `if_any()` returns `FALSE` and +* `if_any()` and `if_all()` are now fully consistent with `any()` and `all()`. + In particular, when called with empty inputs `if_any()` returns `FALSE` and `if_all()` returns `TRUE` (#7059, @jrwinget). # dplyr 1.1.4 diff --git a/R/funs.R b/R/funs.R index 0610b04b51..7653963cc6 100644 --- a/R/funs.R +++ b/R/funs.R @@ -5,14 +5,19 @@ #' #' @details #' `x`, `left`, and `right` are all cast to their common type before the -#' comparison is made. +#' comparison is made. Use the `ptype` argument to specify the type manually. +#' +#' @inheritParams rlang::args_dots_empty #' #' @param x A vector #' @param left,right Boundary values. Both `left` and `right` are recycled to #' the size of `x`. +#' @param ptype An optional prototype giving the desired output type. The +#' default is to compute the common type of `x`, `left`, and `right` using +#' [vctrs::vec_cast_common()]. #' #' @returns -#' A logical vector the same size as `x`. +#' A logical vector the same size as `x` with a type determined by `ptype`. #' #' @seealso #' [join_by()] if you are looking for documentation for the `between()` overlap @@ -27,15 +32,26 @@ #' #' # On a tibble using `filter()` #' filter(starwars, between(height, 100, 150)) -between <- function(x, left, right) { +#' +#' # Using the `ptype` argument with ordered factors, where otherwise everything +#' # is cast to the common type of character before the comparison +#' x <- ordered( +#' c("low", "medium", "high", "medium"), +#' levels = c("low", "medium", "high") +#' ) +#' between(x, "medium", "high") +#' between(x, "medium", "high", ptype = x) +between <- function(x, left, right, ..., ptype = NULL) { + check_dots_empty0(...) + args <- list(x = x, left = left, right = right) # Common type of all inputs - args <- vec_cast_common(!!!args) + args <- vec_cast_common(!!!args, .to = ptype) x <- args$x args$x <- NULL - # But recycle to size of `x` + # Recycle to size of `x` args <- vec_recycle_common(!!!args, .size = vec_size(x)) left <- args$left right <- args$right diff --git a/man/between.Rd b/man/between.Rd index 38b64f198a..334e6f7630 100644 --- a/man/between.Rd +++ b/man/between.Rd @@ -4,16 +4,22 @@ \alias{between} \title{Detect where values fall in a specified range} \usage{ -between(x, left, right) +between(x, left, right, ..., ptype = NULL) } \arguments{ \item{x}{A vector} \item{left, right}{Boundary values. Both \code{left} and \code{right} are recycled to the size of \code{x}.} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{ptype}{An optional prototype giving the desired output type. The +default is to compute the common type of \code{x}, \code{left}, and \code{right} using +\code{\link[vctrs:vec_cast]{vctrs::vec_cast_common()}}.} } \value{ -A logical vector the same size as \code{x}. +A logical vector the same size as \code{x} with a type determined by \code{ptype}. } \description{ This is a shortcut for \code{x >= left & x <= right}, implemented for local @@ -21,7 +27,7 @@ vectors and translated to the appropriate SQL for remote tables. } \details{ \code{x}, \code{left}, and \code{right} are all cast to their common type before the -comparison is made. +comparison is made. Use the \code{ptype} argument to specify the type manually. } \examples{ between(1:12, 7, 9) @@ -31,6 +37,15 @@ x[between(x, -1, 1)] # On a tibble using `filter()` filter(starwars, between(height, 100, 150)) + +# Using the `ptype` argument with ordered factors, where otherwise everything +# is cast to the common type of character before the comparison +x <- ordered( + c("low", "medium", "high", "medium"), + levels = c("low", "medium", "high") +) +between(x, "medium", "high") +between(x, "medium", "high", ptype = x) } \seealso{ \code{\link[=join_by]{join_by()}} if you are looking for documentation for the \code{between()} overlap diff --git a/tests/testthat/_snaps/funs.md b/tests/testthat/_snaps/funs.md index 9cac715c67..5ea5536a10 100644 --- a/tests/testthat/_snaps/funs.md +++ b/tests/testthat/_snaps/funs.md @@ -38,3 +38,12 @@ Error in `between()`: ! Can't recycle `right` (size 2) to size 3. +# ptype argument affects type casting + + Code + between(x, 1.5, 3.5, ptype = integer()) + Condition + Error in `between()`: + ! Can't convert from `left` to due to loss of precision. + * Locations: 1 + diff --git a/tests/testthat/test-funs.R b/tests/testthat/test-funs.R index 94caaac758..205d83a05e 100644 --- a/tests/testthat/test-funs.R +++ b/tests/testthat/test-funs.R @@ -74,6 +74,33 @@ test_that("recycles `left` and `right` to the size of `x`", { }) }) +test_that("ptype argument works as expected with non-alphabetical ordered factors", { + # Create an ordered factor with non-alphabetical order + x <- factor(c("b", "c", "a", "d"), levels = c("d", "c", "b", "a"), ordered = TRUE) + + # Test with ptype specified (uses factor order) + expect_identical( + between(x, "c", "a", ptype = x), + c(TRUE, TRUE, TRUE, FALSE) + ) + + # Test without ptype (uses alphabetical order) + expect_identical( + between(x, "c", "a"), + c(FALSE, FALSE, FALSE, FALSE) + ) +}) + +test_that("ptype argument affects type casting", { + x <- 1:5 + expect_identical( + between(x, 1.5, 3.5), + c(FALSE, TRUE, TRUE, FALSE, FALSE) + ) + expect_snapshot(error = TRUE, { + between(x, 1.5, 3.5, ptype = integer()) + }) +}) # cum* --------------------------------------------------------------------