Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions R/pluralize.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions man/chunks/pluralization.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion man/pluralization-helpers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion man/pluralization.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions man/pluralize.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions tests/testthat/test-pluralization.R
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -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", {
Expand All @@ -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", {
Expand All @@ -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()", {
Expand All @@ -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", {
Expand All @@ -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", {
Expand All @@ -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", {
Expand Down Expand Up @@ -109,22 +116,32 @@ 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(
list("Package{?s}: {pkgs(1)}", "Package: pkg1"),
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", {
expect_error(
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"
)
})