0

I want to do named string formatting, and I don't want to use interpolation because the string will be fetched from CSV.

Suppose I have string data from CSV that I parsed

str = [
   "Hello %{name}, do you come from %{country} ?",
   "Did %{name} just arrived from %{country} ?",
   "Today, %{name} will go to %{country}",
   "Today, %{name} will join us"
]

In Ruby I can do this:

data = {name: "Julio", country: "Nirvana"}

str.each do |s|
  puts str % data
end

# Hello Julio, do you come from Nirvana ?
# Did Julio just arrived from Nirvana ?
# Today, Julio will go to Nirvana ?
# Today, Julio will join us

what is the equivalent in elixir?

I don't mind using hex library

1
  • In ruby String#% is not doing any sanitization for strings, which means for strings it’s an exact equivalent for interpolation. Would you mind to elaborate please? Commented Jan 29, 2023 at 6:54

3 Answers 3

2

You can use a two-level comprehension combined with String.replace/4 to replace the placeholders with their corresponding values.

strings = [
  "Hello %{name}, do you come from %{country} ?",
  "Did %{name} just arrived from %{country} ?",
  "Today, %{name} will go to %{country}",
  "Today, %{name} will join us"
]

data = %{name: "Julio", country: "Nirvana"}

for string <- strings do
  for {key, value} <- data, reduce: string do
    string -> String.replace(string, "%{#{key}}", value)
  end
end

Output:

[
  "Hello Julio, do you come from Nirvana ?",
  "Did Julio just arrived from Nirvana ?",
  "Today, Julio will go to Nirvana",
  "Today, Julio will join us"
]

There is no problem using interpolation here, because we are only interpolating the map keys, not evaluating arbitrary code.


The same thing using Enum.map/2 and Enum.reduce/3 instead of comprehensions:

Enum.map(strings, fn string ->
  Enum.reduce(data, string, fn {key, value}, string ->
    String.replace(string, "%{#{key}}", value)
  end)
end)
Sign up to request clarification or add additional context in comments.

Comments

2

You can use EEx which is part of Elixir and doesn't need to be installed from Hex:

strs = [
   "Hello <%= name %>, do you come from <%=country %> ?",
   "Did <%= name %> just arrived from <%= country %> ?",
   "Today, <%= name %> will go to <%= country %>",
   "Today, <%= name %> will join us"
]

data = [name: "Julio", country: "Nirvana"]

Enum.map(strs, fn str -> EEx.eval_string(str, data) end)

EEx allows to execute arbitrary Elixir code though, so this would be a security risk if you need to work with user-provided CSV files.

1 Comment

oh cool, I completely forgot about EEx formatting, it could be an option
2

You can use Regex.replace/3 to do the replacements in one go. This would be more efficient than @Adam's for + String.replace/3 solution, especially if there a large number of elements in data:

strings = [
  "Hello %{name}, do you come from %{country} ?",
  "Did %{name} just arrived from %{country} ?",
  "Today, %{name} will go to %{country}",
  "Today, %{name} will join us"
]

data = %{"name" => "Julio", "country" => "Nirvana"}

for string <- strings do
  Regex.replace(~r/%{(\w+)}/, string, fn _, key -> Map.fetch!(data, key) end)
end
|> IO.inspect()

Output:

["Hello Julio, do you come from Nirvana ?",
 "Did Julio just arrived from Nirvana ?",
 "Today, Julio will go to Nirvana",
 "Today, Julio will join us"]

The regex /%{(\w+)}/ matches %{ followed by one or more letters or numbers (\w+) followed by }, capturing the letter/number sequence, which we use in the second argument of the replacement callback to fetch the right replacement value from data.


One more difference compared to @Adam's solution is that this will raise an error if the string contains an interpolation that does not exist in data instead of silently ignoring it:

strings = ["Today, %{name} will go to %{city}"]

raises

** (KeyError) key "city" not found in: %{"country" => "Nirvana", "name" => "Julio"}
    :erlang.map_get("city", %{"country" => "Nirvana", "name" => "Julio"})

(You can handle such errors in a different way if you want by changing what the replacement function does.)

2 Comments

If there are more keys, sure, but with the provided example, Regex is twice as slow on my Core i5 (it starts to win at about 8 keys). With Elixir there are so many nice built-in options I always treat Regex a last resort.
Yup, for two, String.replace should certainly be faster. Still, a benefit of this method is that it lets you catch typos inside %{}.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.