5
votes

When using rmarkdown it is frequently the case that you want to programmatically generate fragments of text, in particular to list items being used. For example;

The species of iris examined were `r cat(as.character(unique(iris$Species)), sep = ", ")`.

Which would produce

The species of iris examined were setosa, versicolor, virginica.

To read properly it should be

The species of iris examined were setosa, versicolor, and virginica.

Is there a simple way to do this?

4
Maybe try retrieving your values beforehand and then using your current formatting for all but the last value and then concatenate that with an "and" and the final value of your list?ydaetskcoR
Yeah - making a function to do this would't be too hard. You could even have an oxford.comma parameter. That should clearly default to true ;)Dason
why would one not use an oxford comma? "setosa, veriscolor and virginica" imply that versicolor and virginica are both setosa which is probably not what you wantrawr
Added oxford comma. Happy now? :-)Corvus
not until there is peace on earthrawr

4 Answers

9
votes

This is one of the helpful tools in the pander package

pander::p

p merges elements of a vector in one string for the sake of pretty inline printing. Default parameters are read from appropriate option values (see argument description for details). This function allows you to put the results of an expression that yields a variable inline, by wrapping the vector elements with the string provided in wrap, and separating elements by main and ending separator (sep and copula). In case of a two-length vector, value specified in copula will be used as a separator. You can also control the length of provided vector by altering an integer value specified in limit argument (defaults to Inf).

example:

devtools::install_github('Rapporter/pander')
## also available on cran:
# install.packages('pander')

library(pander)

p(levels(iris$Species), wrap = '')
# "setosa, versicolor and virginica"

p(levels(iris$Species), wrap = '', copula = ', and ')
# "setosa, versicolor, and virginica"
5
votes

Here's such a function

wordlist<-function(w, oxford=F) {
    if(length(w)==1) return(w);
    if(length(w)==2) return(paste(w[1],"and",w[2]));
    paste0( paste(w[-length(w)], collapse=", "), 
        ifelse(oxford,",","")," and ", w[length(w)] )
}

wordlist(unique(iris$Species))
# [1] "setosa, versicolor and virginica"

(with oxford set to false per the OP's example)

4
votes

Recursive function to handle the final two elements differently:

wordlist <- function(w) { 
                          if (length(w) <= 2) {
                            paste(w, collapse=' and ') # Or collapse=', and '
                          } else {
                            paste(w[1], Recall(w[-1]), sep=', ')
                          }
                        }
wordlist(LETTERS[1:6])
## [1] "A, B, C, D, E and F"
3
votes

Try this:

toStringAnd <- function(s) {
    n <- length(s)
    if (n < 2) s else toString(s[-n], paste("and", s[n]))
}

# test
toStringAnd( tail(LETTERS) )
## [1] "U, V, W, X, Y, and Z"

Note: The above answers the question but just in case you change your mind and decide not to have a comma before the and then:

toStringAnd2 <- function(s) {
    n <- length(s)
    if (n < 2) s else paste(toString(s[-n]), "and", s[n])
}

# test
toStringAnd2( tail(LETTERS) )
## [1] "U, V, W, X, Y and Z"

Other variations are possible depending on what you want such as not using a comma if there are only two input components and using a comma if there are more than two but the general pattern of combining toString and paste should be clear at this point.

Update Added Note and some improvements.