1#' Format a string.
2#'
3#' Perform a string formatting operation.
4#'
5#' The string on which this method is called can contain
6#' literal text or replacement fields delimited by braces \code{\{\}}. Each replacement field contains
7#' either the numeric index of a positional argument, or the name of a keyword argument. Returns
8#' a copy of the string where each replacement field is replaced with the string value of the
9#' corresponding argument.
10#'
11#' If \code{...} is a single argument of a \code{data.frame}-like object, \code{pystr_format} will
12#' return an \code{nrow()}-length character vector using the column names of the data.frame for
13#' the named \code{\{placeholder\}}s.
14#'
15#' @param str A character vector.
16#' @param ... Parameter values. See details and examples
17#'
18#' @return A character vector.
19#'
20#' @references \url{https://docs.python.org/3/library/stdtypes.html#str.format}
21#'
22#' @examples
23#' # Numeric placeholders
24#'
25#' pystr_format("Hello {1}, my name is {2}.", "World", "Nicole")
26#' pystr_format("Hello {1}, my name is {2}.", c("World", "Nicole"))
27#' pystr_format("Hello {1}, my name is {2}.", list("World", "Nicole"))
28#'
29#' # Named placeholders
30#'
31#' pystr_format("Hello {thing}, my name is {name}.", thing="World", name="Nicole")
32#' pystr_format("Hello {thing}, my name is {name}.", c(thing="World", name="Nicole"))
33#' pystr_format("Hello {thing}, my name is {name}.", list(thing="World", name="Nicole"))
34#'
35#' # Pass in characters and numbers
36#'
37#' pystr_format("Hello {name}, you have {n} new notifications!", name="Nicole", n=2)
38#'
39#' ## Placeholders can be used more than once
40#'
41#' pystr_format("The name is {last}. {first} {last}.", last="Bond", first="James")
42#'
43#' ## Pass in a whole data frame, matching by column names
44#'
45#' my_cars <- data.frame(car=rownames(mtcars), mtcars)
46#' head(pystr_format("The {car} gets {mpg} mpg (hwy) despite having {cyl} cylinders.", my_cars))
47#'
48#' supers <- data.frame(first=c("Bruce", "Hal", "Clark", "Diana"),
49#'                      last=c("Wayne", "Jordan", "Kent", "Prince"),
50#'                      is=c("Batman", "Green Lantern", "Superman", "Wonder Woman"))
51#' pystr_format("{first} {last} is really {is} but you shouldn't call them {first} in public.", supers)
52#'
53#' @export
54pystr_format <- function(str, ...) {
55  args = list(...)
56  return(sapply(str, function(x) pystr_format_(x, args), USE.NAMES = FALSE))
57}
58
59pystr_format_ <- function(str, args) {
60  # if nothing was passed in besides 'str'
61  if(length(args) == 0) {
62    return(str)
63  }
64
65  params = args
66
67  if(length(args) == 1) {
68
69    if (inherits(args[[1]], "data.frame")) {
70
71      # convert whatever else it may be besides a data.frame to a data.frame
72      # to avoid return type issues with tbl_'s and with= nonsense with data.table
73      df <- data.frame(args[[1]], stringsAsFactors=FALSE, check.names=FALSE)
74
75      pat <-  "\\{[[:alnum:]]+\\}"
76      looking_for <- gsub("[\\{\\}]", "", regmatches(str, gregexpr(pat, str))[[1]])
77      has <- colnames(df)
78      will_replace <- intersect(looking_for, has)
79      if (length(will_replace) > 0) {
80        sapply(1:nrow(df), function(i) {
81          res <- str
82          for(repl in will_replace) res <- gsub(sprintf("\\{%s\\}", repl), df[i, repl], res)
83          res
84        }) -> out
85        return(out)
86
87      }
88
89    }
90
91    if(is.null(names(args))) {
92      params = args[[1]]
93    }
94  }
95
96  if(length(params) == 0) {
97    return(str)
98  }
99
100  if(is.null(names(params))) {
101    names(params) = 1:length(params)
102  }
103
104  for(i in 1:length(params)) {
105    str = gsub(paste0("\\{", names(params[i]), "\\}"), params[[i]], str)
106  }
107
108  return(str)
109}
110