r-lib / cli
1

2
#' CLI pluralization
3
#'
4
#' @name pluralization
5
#' @family pluralization
6
#' @includeRmd man/chunks/pluralization.Rmd
7
NULL
8

9
make_quantity <- function(object) {
10 1
  val <- if (is.numeric(object)) {
11 1
    stopifnot(length(object) == 1)
12 1
    as.integer(object)
13
  } else {
14 1
    length(object)
15
  }
16
}
17

18
#' Pluralization helper functions
19
#'
20
#' @rdname pluralization-helpers
21
#' @param expr For `no()` it is an expression that is printed as "no" in
22
#'   cli expressions, it is interpreted as a zero quantity. For `qty()`
23
#'   an expression that sets the pluralization quantity without printing
24
#'   anything. See examples below.
25
#'
26
#' @export
27
#' @family pluralization
28

29
no <- function(expr) {
30 1
  stopifnot(is.numeric(expr), length(expr) == 1, !is.na(expr))
31 1
  structure(
32 1
    expr,
33 1
    class = "cli_no"
34
  )
35
}
36

37
#' @export
38

39
as.character.cli_no <- function(x, ...) {
40 1
  if (make_quantity(x) == 0) "no" else as.character(unclass(x))
41
}
42

43
#' @rdname pluralization-helpers
44
#' @export
45

46
qty <- function(expr) {
47 1
  structure(
48 1
    make_quantity(expr),
49 1
    class = "cli_noprint"
50
  )
51
}
52

53
#' @export
54

55
as.character.cli_noprint <- function(x, ...) {
56
  ""
57
}
58

59
parse_plural <- function(code, values) {
60
  # If we have the quantity already, then process it now.
61
  # Otherwise we put in a marker for it, and request post-processing.
62 1
  qty <- make_quantity(values$qty)
63 1
  if (!is.na(qty)) {
64 1
    process_plural(qty, code)
65
  } else {
66 1
    values$postprocess <- TRUE
67 1
    id <- random_id()
68 1
    values$pmarkers[[id]] <- code
69 1
    id
70
  }
71
}
72

73
process_plural <- function(qty, code) {
74 1
  parts <- strsplit(str_tail(code), "/", fixed = TRUE)[[1]]
75 1
  if (last_character(code) == "/") parts <- c(parts, "")
76 1
  if (length(parts) == 1) {
77 1
    if (qty != 1) parts[1] else ""
78 1
  } else if (length(parts) == 2) {
79 1
    if (qty == 1) parts[1] else parts[2]
80 1
  } else if (length(parts) == 3) {
81 1
    if (qty == 0) {
82 1
      parts[1]
83 1
    } else if (qty == 1) {
84 1
      parts[2]
85
    } else {
86 1
      parts[3]
87
    }
88
  } else {
89 0
    stop("Invalid pluralization directive: `", code, "`")
90
  }
91
}
92

93
post_process_plurals <- function(str, values) {
94 1
  if (!values$postprocess) return(str)
95 1
  if (values$num_subst == 0) {
96 1
    stop("Cannot pluralize without a quantity")
97
  }
98 1
  if (values$num_subst != 1) {
99 1
    stop("Multiple quantities for pluralization")
100
  }
101

102 1
  qty <- make_quantity(values$qty)
103 1
  for (i in seq_along(values$pmarkers)) {
104 1
    mark <- values$pmarkers[i]
105 1
    str <- sub(names(mark), process_plural(qty, mark[[1]]), str)
106
  }
107

108 1
  str
109
}
110

111
#' String templating with pluralization
112
#'
113
#' `pluralize()` is similar to [glue::glue()], with two differences:
114
#' * It supports cli's [pluralization] syntax, using `{?}` markers.
115
#' * It collapses substituted vectors into a comma separated string.
116
#'
117
#' See [pluralization] and some examples below.
118
#'
119
#' @param ...,.envir,.transformer All arguments are passed to [glue::glue()].
120
#'
121
#' @export
122
#' @family pluralization
123
#' @examples
124
#' # Regular plurals
125
#' nfile <- 0; pluralize("Found {nfile} file{?s}.")
126
#' nfile <- 1; pluralize("Found {nfile} file{?s}.")
127
#' nfile <- 2; pluralize("Found {nfile} file{?s}.")
128
#'
129
#' # Irregular plurals
130
#' ndir <- 1; pluralize("Found {ndir} director{?y/ies}.")
131
#' ndir <- 5; pluralize("Found {ndir} director{?y/ies}.")
132
#'
133
#' # Use 'no' instead of zero
134
#' nfile <- 0; pluralize("Found {no(nfile)} file{?s}.")
135
#' nfile <- 1; pluralize("Found {no(nfile)} file{?s}.")
136
#' nfile <- 2; pluralize("Found {no(nfile)} file{?s}.")
137
#'
138
#' # Use the length of character vectors
139
#' pkgs <- "pkg1"
140
#' pluralize("Will remove the {pkgs} package{?s}.")
141
#' pkgs <- c("pkg1", "pkg2", "pkg3")
142
#' pluralize("Will remove the {pkgs} package{?s}.")
143
#'
144
#' pkgs <- character()
145
#' pluralize("Will remove {?no/the/the} {pkgs} package{?s}.")
146
#' pkgs <- c("pkg1", "pkg2", "pkg3")
147
#' pluralize("Will remove {?no/the/the} {pkgs} package{?s}.")
148
#'
149
#' # Multiple quantities
150
#' nfiles <- 3; ndirs <- 1
151
#' pluralize("Found {nfiles} file{?s} and {ndirs} director{?y/ies}")
152
#'
153
#' # Explicit quantities
154
#' nupd <- 3; ntotal <- 10
155
#' cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates")
156

157
pluralize <- function(..., .envir = parent.frame(),
158
                      .transformer = glue::identity_transformer) {
159

160 1
  values <- new.env(parent = emptyenv())
161 1
  values$empty <- random_id()
162 1
  values$qty <- values$empty
163 1
  values$num_subst <- 0L
164 1
  values$postprocess <- FALSE
165 1
  values$pmarkers <- list()
166

167 1
  tf <- function(text, envir) {
168 1
    if (substr(text, 1, 1) == "?") {
169 1
      if (identical(values$qty, values$empty)) {
170 1
        values$postprocess <- TRUE
171 1
        id <- random_id()
172 1
        values$pmarkers[[id]] <- text
173 1
        return(id)
174
      } else {
175 1
        return(process_plural(make_quantity(values$qty), text))
176
      }
177

178
    } else {
179 1
      values$num_subst <- values$num_subst + 1
180 1
      qty <- .transformer(text, envir)
181 1
      values$qty <- qty
182 1
      return(inline_collapse(qty))
183
    }
184
  }
185

186 1
  raw <- glue::glue(..., .envir = .envir, .transformer = tf)
187 1
  post_process_plurals(raw, values)
188
}

Read our documentation on viewing source code .

Loading