diff --git a/NAMESPACE b/NAMESPACE index 504032d1..cd85227f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -30,6 +30,7 @@ export(fct_relabel) export(fct_relevel) export(fct_reorder) export(fct_reorder2) +export(fct_reordern) export(fct_rev) export(fct_shift) export(fct_shuffle) diff --git a/NEWS.md b/NEWS.md index e0e07694..0ddba6e0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,6 @@ # forcats (development version) +* `fct_reordern()` is a new function to order based on an arbitrary number of + values (@billdenney, #16) # forcats 0.5.2 diff --git a/R/reorder.R b/R/reorder.R index 0ebc2a95..c2de42eb 100644 --- a/R/reorder.R +++ b/R/reorder.R @@ -17,6 +17,8 @@ #' @param .desc Order in descending order? Note the default is different #' between `fct_reorder` and `fct_reorder2`, in order to #' match the default ordering of factors in the legend. +#' @importFrom stats median +#' @family Reordering #' @export #' @examples #' df <- tibble::tribble( @@ -73,6 +75,26 @@ fct_reorder2 <- function(.f, .x, .y, .fun = last2, ..., .desc = TRUE) { lvls_reorder(.f, order(summary, decreasing = .desc)) } +#' Reorder factor levels by sorting along multiple variables +#' +#' @param .f A factor (or character vector). +#' @param ordered Passed to \code{\link{fct_inorder}()} +#' @inheritDotParams base::order +#' @family Reordering +#' @examples +#' A <- c(3, 3, 2, 1) +#' B <- c("A", "B", "C", "D") +#' fct_reordern(c("A", "B", "C", "D"), A, B) +#' fct_reordern(c("A", "B", "C", "D"), dplyr::desc(A), dplyr::desc(B)) +#' fct_reordern(c("A", "B", "C", "D"), A, dplyr::desc(B)) +#' @export +fct_reordern <- function(.f, ..., ordered = NA) { + f <- check_factor(.f) + new_order <- base::order(...) + idx <- as.integer(f)[new_order] + lvls_reorder(f, idx = idx[!duplicated(idx)], ordered = ordered) +} + check_single_value_per_group <- function(x, fun_arg, call = caller_env()) { # This is a bit of a weak test, but should detect the most common case # where `.fun` returns multiple values. diff --git a/_pkgdown.yml b/_pkgdown.yml index e041f423..0bc7a75d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -25,6 +25,7 @@ reference: - fct_relevel - fct_inorder - fct_reorder + - fct_reordern - fct_infreq - fct_shuffle - fct_rev diff --git a/man/fct_reorder.Rd b/man/fct_reorder.Rd index 4afef0c3..d7c8d373 100644 --- a/man/fct_reorder.Rd +++ b/man/fct_reorder.Rd @@ -71,3 +71,8 @@ if (require("ggplot2")) { labs(colour = "Chick") } } +\seealso{ +Other Reordering: +\code{\link{fct_reordern}()} +} +\concept{Reordering} diff --git a/man/fct_reordern.Rd b/man/fct_reordern.Rd new file mode 100644 index 00000000..a7ec7ff8 --- /dev/null +++ b/man/fct_reordern.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/reorder.R +\name{fct_reordern} +\alias{fct_reordern} +\title{Reorder factor levels by sorting along multiple variables} +\usage{ +fct_reordern(.f, ..., ordered = NA) +} +\arguments{ +\item{.f}{A factor (or character vector).} + +\item{...}{ + Arguments passed on to \code{\link[base:order]{base::order}} + \describe{ + \item{\code{decreasing}}{logical. Should the sort order be increasing or + decreasing? For the \code{"radix"} method, this can be a vector of + length equal to the number of arguments in \code{\dots}. For the + other methods, it must be length one.} + \item{\code{na.last}}{for controlling the treatment of \code{NA}s. + If \code{TRUE}, missing values in the data are put last; if + \code{FALSE}, they are put first; if \code{NA}, they are removed + (see \sQuote{Note}.)} + \item{\code{method}}{the method to be used: partial matches are allowed. The + default (\code{"auto"}) implies \code{"radix"} for short numeric vectors, + integer vectors, logical vectors and factors. + Otherwise, it implies \code{"shell"}. + For details of methods \code{"shell"}, + \code{"quick"}, and \code{"radix"}, + see the help for \code{\link[base]{sort}}.} + }} + +\item{ordered}{Passed to \code{\link{fct_inorder}()}} +} +\description{ +Reorder factor levels by sorting along multiple variables +} +\examples{ +A <- c(3, 3, 2, 1) +B <- c("A", "B", "C", "D") +fct_reordern(c("A", "B", "C", "D"), A, B) +fct_reordern(c("A", "B", "C", "D"), dplyr::desc(A), dplyr::desc(B)) +fct_reordern(c("A", "B", "C", "D"), A, dplyr::desc(B)) +} +\seealso{ +Other Reordering: +\code{\link{fct_reorder}()} +} +\concept{Reordering} diff --git a/tests/testthat/test-reorder.R b/tests/testthat/test-reorder.R index 2eb4e46c..6aa2fcdf 100644 --- a/tests/testthat/test-reorder.R +++ b/tests/testthat/test-reorder.R @@ -93,3 +93,29 @@ test_that("fct_inseq gives error for non-numeric levels", { f <- factor(c("c", "a", "a", "b")) expect_error(levels(fct_inseq(f)), "level must be coercible to numeric") }) + +test_that("fct_reordern works for all scenarios (fix #16)", { + A <- c(3, 3, 2, 1) + B <- c("A", "B", "C", "D") + f <- c("A", "B", "C", "D") + f_factor_ordered <- factor(f, ordered = TRUE) + expect_equal( + fct_reordern(f, A, B), + factor(f, levels = c("D", "C", "A", "B")) + ) + # Ensure interaction with dplyr::desc() is accurate. + desc <- function(x) -xtfrm(x) + expect_equal( + fct_reordern(f, A, desc(B)), + factor(f, levels = c("D", "C", "B", "A")) + ) + # Checks of ordering + expect_equal( + fct_reordern(f, A, B, ordered = NA), + factor(f, levels = c("D", "C", "A", "B")) + ) + expect_equal( + fct_reordern(f, A, B, ordered = TRUE), + factor(f, levels = c("D", "C", "A", "B"), ordered = TRUE) + ) +})