diff --git a/NAMESPACE b/NAMESPACE index dcd086a3f..0a17072bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -87,6 +87,7 @@ export(list_symbols) export(make_ansi_style) export(make_spinner) export(no) +export(pluralize) export(qty) export(rule) export(simple_theme) diff --git a/NEWS.md b/NEWS.md index 350791b49..902bffff8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,9 @@ * `console_width()` works better now in RStudio, and also in terminals. +* New `pluralize()` function to perform pluralization without generating + cli output (#155). + # cli 2.0.2 * The status bar now does not simplify multiple spaces by a single space. diff --git a/R/pluralize.R b/R/pluralize.R index f296894b5..3a366fd53 100644 --- a/R/pluralize.R +++ b/R/pluralize.R @@ -106,3 +106,82 @@ post_process_plurals <- function(str, values) { str } + +#' String templating with pluralization +#' +#' `pluralize()` is similar to [glue::glue()], with two differences: +#' * It supports cli's [pluralization] syntax, using `{?}` markers. +#' * It collapses substituted vectors into a comma separated string. +#' +#' See [pluralization] and some examples below. +#' +#' @param ...,.envir,.transformer All arguments are passed to [glue::glue()]. +#' +#' @export +#' @family pluralization +#' @examples +#' # Regular plurals +#' nfile <- 0; pluralize("Found {nfile} file{?s}.") +#' nfile <- 1; pluralize("Found {nfile} file{?s}.") +#' nfile <- 2; pluralize("Found {nfile} file{?s}.") +#' +#' # Irregular plurals +#' ndir <- 1; pluralize("Found {ndir} director{?y/ies}.") +#' ndir <- 5; pluralize("Found {ndir} director{?y/ies}.") +#' +#' # Use 'no' instead of zero +#' nfile <- 0; pluralize("Found {no(nfile)} file{?s}.") +#' nfile <- 1; pluralize("Found {no(nfile)} file{?s}.") +#' nfile <- 2; pluralize("Found {no(nfile)} file{?s}.") +#' +#' # Use the length of character vectors +#' pkgs <- "pkg1" +#' pluralize("Will remove the {pkgs} package{?s}.") +#' pkgs <- c("pkg1", "pkg2", "pkg3") +#' pluralize("Will remove the {pkgs} package{?s}.") +#' +#' pkgs <- character() +#' pluralize("Will remove {?no/the/the} {pkgs} package{?s}.") +#' pkgs <- c("pkg1", "pkg2", "pkg3") +#' pluralize("Will remove {?no/the/the} {pkgs} package{?s}.") +#' +#' # Multiple quantities +#' nfiles <- 3; ndirs <- 1 +#' pluralize("Found {nfiles} file{?s} and {ndirs} director{?y/ies}") +#' +#' # Explicit quantities +#' nupd <- 3; ntotal <- 10 +#' cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates") + +pluralize <- function(..., .envir = parent.frame(), + .transformer = glue::identity_transformer) { + + values <- new.env(parent = emptyenv()) + values$empty <- random_id() + values$qty <- values$empty + values$num_subst <- 0L + values$postprocess <- FALSE + values$pmarkers <- list() + + tf <- function(text, envir) { + if (substr(text, 1, 1) == "?") { + if (identical(values$qty, values$empty)) { + values$postprocess <- TRUE + id <- random_id() + values$pmarkers[[id]] <- text + return(id) + } else { + return(process_plural(make_quantity(values$qty), text)) + } + + } else { + values$num_subst <- values$num_subst + 1 + qty <- .transformer(text, envir) + values$qty <- qty + return(inline_collapse(qty)) + } + } + + raw <- glue::glue(..., .envir = .envir, .transformer = tf) + post_process_plurals(raw, values) +} diff --git a/man/chunks/pluralization.Rmd b/man/chunks/pluralization.Rmd index fe9ca18ff..93214fabf 100644 --- a/man/chunks/pluralization.Rmd +++ b/man/chunks/pluralization.Rmd @@ -20,6 +20,9 @@ pluralization examples that you can use as guidelines. Hopefully these are intuitive enough, so that they can be used without knowing the exact cli pluralization rules. +If you need pluralization without the semantic cli functions, see the +`pluralize()` function. + # Examples ## Pluralization markup diff --git a/man/pluralization-helpers.Rd b/man/pluralization-helpers.Rd index f76b95070..f9700edd5 100644 --- a/man/pluralization-helpers.Rd +++ b/man/pluralization-helpers.Rd @@ -20,6 +20,7 @@ Pluralization helper functions } \seealso{ Other pluralization: -\code{\link{pluralization}} +\code{\link{pluralization}}, +\code{\link{pluralize}()} } \concept{pluralization} diff --git a/man/pluralization.Rd b/man/pluralization.Rd index e973eb824..662599245 100644 --- a/man/pluralization.Rd +++ b/man/pluralization.Rd @@ -13,6 +13,9 @@ increases the quality of the messages greatly. In this document we first show some pluralization examples that you can use as guidelines. Hopefully these are intuitive enough, so that they can be used without knowing the exact cli pluralization rules. + +If you need pluralization without the semantic cli functions, see the +\code{pluralize()} function. } \section{Examples}{ @@ -154,6 +157,7 @@ otherwise. \seealso{ Other pluralization: -\code{\link{no}()} +\code{\link{no}()}, +\code{\link{pluralize}()} } \concept{pluralization} diff --git a/man/pluralize.Rd b/man/pluralize.Rd new file mode 100644 index 000000000..fbd409e7e --- /dev/null +++ b/man/pluralize.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pluralize.R +\name{pluralize} +\alias{pluralize} +\title{String templating with pluralization} +\usage{ +pluralize( + ..., + .envir = parent.frame(), + .transformer = glue::identity_transformer +) +} +\arguments{ +\item{..., .envir, .transformer}{All arguments are passed to \code{\link[glue:glue]{glue::glue()}}.} +} +\description{ +\code{pluralize()} is similar to \code{\link[glue:glue]{glue::glue()}}, with two differences: +\itemize{ +\item It supports cli's \link{pluralization} syntax, using \verb{\{?\}} markers. +\item It collapses substituted vectors into a comma separated string. +} +} +\details{ +See \link{pluralization} and some examples below. +} +\examples{ +# Regular plurals +nfile <- 0; pluralize("Found {nfile} file{?s}.") +nfile <- 1; pluralize("Found {nfile} file{?s}.") +nfile <- 2; pluralize("Found {nfile} file{?s}.") + +# Irregular plurals +ndir <- 1; pluralize("Found {ndir} director{?y/ies}.") +ndir <- 5; pluralize("Found {ndir} director{?y/ies}.") + +# Use 'no' instead of zero +nfile <- 0; pluralize("Found {no(nfile)} file{?s}.") +nfile <- 1; pluralize("Found {no(nfile)} file{?s}.") +nfile <- 2; pluralize("Found {no(nfile)} file{?s}.") + +# Use the length of character vectors +pkgs <- "pkg1" +pluralize("Will remove the {pkgs} package{?s}.") +pkgs <- c("pkg1", "pkg2", "pkg3") +pluralize("Will remove the {pkgs} package{?s}.") + +pkgs <- character() +pluralize("Will remove {?no/the/the} {pkgs} package{?s}.") +pkgs <- c("pkg1", "pkg2", "pkg3") +pluralize("Will remove {?no/the/the} {pkgs} package{?s}.") + +# Multiple quantities +nfiles <- 3; ndirs <- 1 +pluralize("Found {nfiles} file{?s} and {ndirs} director{?y/ies}") + +# Explicit quantities +nupd <- 3; ntotal <- 10 +cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates") +} +\seealso{ +Other pluralization: +\code{\link{no}()}, +\code{\link{pluralization}} +} +\concept{pluralization} diff --git a/tests/testthat/test-pluralization.R b/tests/testthat/test-pluralization.R index 374edbf84..021e31c9c 100644 --- a/tests/testthat/test-pluralization.R +++ b/tests/testthat/test-pluralization.R @@ -11,6 +11,7 @@ test_that("simplest", { list("{2} package{?s}", "2 packages") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("irregular", { @@ -20,6 +21,7 @@ test_that("irregular", { list("{2} dictionar{?y/ies}", "2 dictionaries") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("multiple substitutions", { @@ -29,6 +31,7 @@ test_that("multiple substitutions", { list("{2} package{?s} {?is/are} ...", "2 packages are ...") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("multiple quantities", { @@ -44,6 +47,7 @@ test_that("multiple quantities", { list("{2} package{?s} and {2} folder{?s}", "2 packages and 2 folders") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("no()", { @@ -53,6 +57,7 @@ test_that("no()", { list("{no(2)} package{?s}", "2 packages") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("set qty() explicitly", { @@ -62,6 +67,7 @@ test_that("set qty() explicitly", { list("{qty(2)}There {?is/are} {2} package{?s}", "There are 2 packages") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("collapsing vectors", { @@ -72,6 +78,7 @@ test_that("collapsing vectors", { list("The {pkgs(3)} package{?s}", "The pkg1, pkg2, and pkg3 packages") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("pluralization and style", { @@ -109,6 +116,7 @@ test_that("post-processing", { list("Package{?s}: {2}", "Packages: 2") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) pkgs <- function(n) glue("pkg{seq_len(n)}") cases <- list( @@ -116,6 +124,7 @@ test_that("post-processing", { list("Package{?s}: {pkgs(2)}", "Packages: pkg1 and pkg2") ) for (c in cases) expect_equal(str_trim(capt0(cli_text(c[[1]]))), c[[2]]) + for (c in cases) expect_equal(pluralize(c[[1]]), c[[2]]) }) test_that("post-processing errors", { @@ -123,8 +132,16 @@ test_that("post-processing errors", { cli_text("package{?s}"), "Cannot pluralize without a quantity" ) + expect_error( + pluralize("package{?s}"), + "Cannot pluralize without a quantity" + ) expect_error( cli_text("package{?s} {5} {10}"), "Multiple quantities for pluralization" ) + expect_error( + pluralize("package{?s} {5} {10}"), + "Multiple quantities for pluralization" + ) })