Julien Navarre
Julien Navarre

Reputation: 7830

Evaluate an object into a string

I have a string with parameters inside braces, I set these parameters as objects and I want to evaluate them and replace them by their value in my string.

Here is what I did but I don't like the way I did it and maybe I can evaluate my parameters faster.

(I used ls() but I should create a data.frame for my values then I can use them easily).

region <- "france"
name <- "julien"

str <- "/test/{region}/v1.1/{name}/{test}"

df <- data.frame(object = gsub("[\\{\\}]", "", regmatches(str, gregexpr("\\{.*?\\}", str))[[1]]), string = unlist(regmatches(str, gregexpr("\\{.*?\\}", str))), stringsAsFactors = FALSE)

> df
  object   string
1 region {region}
2   name   {name}
3   test   {test}

for(i in 1:nrow(df)){
  if (df$object[i] %in% ls()){
    df$value[i] <- eval(as.name(df$object[i]))
  } else {
    df$value[i] <- ""
  }
  str <- gsub(df$string[i], df$value[i], str, fixed = TRUE)
}


> df
  object   string  value
1 region {region} france
2   name   {name} julien
3   test   {test}       
> 
> str
[1] "/test/france/v1.1/julien/"

If someone have an idea to improve the code and make it more efficient and cleaner (or evaluate directly my parameters in the string), thank you for any help.

Upvotes: 0

Views: 129

Answers (4)

Karl Forner
Karl Forner

Reputation: 4414

You could use gsubfn:

library(gsubfn)
region <- "france"
name <- "julien"
test <- 'toto'

str <- "/test/{region}/v1.1/{name}/{test}"
gsubfn('\\{(\\w+)\\}', get, str)
[1] "/test/france/v1.1/julien/toto"

If you want to pick your variables from a data frame:

df <- data.frame(region = 'France', name = 'Julien', test = 'Success', 
  stringsAsFactors = FALSE)
gsubfn('\\{(\\w+)\\}', function(x) get(x, df), str)

or

gsubfn('\\{(\\w+)\\}', x ~ get(x, df), str)

or even just:

gsubfn('\\{(\\w+)\\}', df, str)

It also works with a list:

L <- list(region = 'France', name = 'Julien', test = 'Success')
gsubfn('\\{(\\w+)\\}', L, str)

Upvotes: 1

Richie Cotton
Richie Cotton

Reputation: 121077

A good rule of thumb when dealing with strings is to never, ever use the builtin regular expression functions, if you can help it. Instead, use the stringr package since it makes your code cleaner.

In this case, you can simplify the gregexpr/regmatches mess with a call to str_match_all.
The parentheses, (, show the region to be captured: "at least one alphabetic character", via [[:alpha:]]+. This is returned in the second column.
The first column contains the full match, which also includes the curly braces, {.

library(stringr)
matches <- str_match_all(str, "\\{([[:alpha:]]+)\\}")[[1]]
colnames(matches) <- c("string", "object")
matches
##     string     object  
## [1,] "{region}" "region"
## [2,] "{name}"   "name"  
## [3,] "{test}"   "test"

Then proceed as per Roland's answer, using a lookup data frame.

lookup <- data.frame(
  object = c("region", "name"),
  value  = c("france", "julien")
)

(df <- merge(matches, lookup, all.x = TRUE))
##  object   string  value
## 1   name   {name} julien
## 2 region {region} france
## 3   test   {test}   <NA>

Update regarding replacing values:

Since the values need to be updated sequentially rather than all at once, a for loop is as good as anything. There are a couple of minor improvements that you can make. 1:nrow(df) is a bad idea if it is possible for df to have zero rows, since 1:0 is not what you want. str_replace_all is a little easier on the eye than gsub.

First, a couple of changes to the data frame. The string column should be a charcter vector rather than a factor, and you want empty strings instead of missing values.

df <- within(
  df,
  {
    string <- as.character(df$string)
    value <- ifelse(is.na(value), "", value)   
  }
)

The updated loop looks like:

str <- "/test/{region}/v1.1/{name}/{test}"

for(i in seq_len(nrow(df))) 
{
  str <- with(df, str_replace_all(str, fixed(string[i]), value[i]))
}
str
## [1] "/test/france/v1.1/julien/"

Upvotes: 1

Sven Hohenstein
Sven Hohenstein

Reputation: 81693

An easier way to get the values can be achieved by get and exists:

df$value <- sapply(df$object, function(x) if (exists(x)) get(x) else "")

#   object   string  value
# 1 region {region} france
# 2   name   {name} julien
# 3   test   {test}       

An alternative way (without the data frame):

str <- "/test/{region}/v1.1/{name}/{test}"

matches <- regmatches(str, 
                      gregexpr("(?<=\\{)\\w+(?=\\})", str, perl = TRUE))[[1]]
values <- sapply(matches, function(x) if (exists(x)) get(x) else "")

for (i in seq_along(matches)) {
  str <- sub(paste0("\\{", matches[i], "\\}"), values[i], str)  
}

str
# [1] "/test/france/v1.1/julien/"

Upvotes: 1

Roland
Roland

Reputation: 132706

You could use get or mget, since eval is evil. However, a better strategy than having all those objects flying around in your global environment would be creating a look-up table:

df1 <- data.frame(object=c("region", "name"),
                  value=c("frace", "julien"))

Then you could use merge:

merge(df, df1, all=TRUE)

Upvotes: 2

Related Questions