Kevin Tanudjaja
Kevin Tanudjaja

Reputation: 886

How to do string named format in Elixir without interpolation?

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

Upvotes: 0

Views: 220

Answers (3)

Dogbert
Dogbert

Reputation: 222060

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.)

Upvotes: 2

Adam Millerchip
Adam Millerchip

Reputation: 23091

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)

Upvotes: 2

sabiwara
sabiwara

Reputation: 3134

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.

Upvotes: 1

Related Questions