What are the best practices to substitute variables in a string

So I have a string something like

str = “Hello @contact.name, how are you. It looks like your age is @contact.age”. So the solution of your problem is @solution.age.

After replacing -

str = “Hello Rahul, how are you. It looks like your age is 27”. So the solution to your problem is Eat healthily.

I am just curious on what are the most effective way to implement this in Elixir. If somebody can share his thoughts that would be really helpful.

1 Like

Interpolation:

iex> foo = “bar”
iex> “Hello, #{foo}”
“Hello, bar”
1 Like

Thanks for the quick reply, I know that but I have a list of variables.
So to be bit more specific I was looking for something cleaner like EEx.eval_string() for other symbol like @

You might look at

EEx.eval_string

Something like…

iex> template = "Hello <%= contact.name %>, how are you. It looks like your age is <%= contact.age %>. So the solution of your problem is <%= solution.age %>."
iex> EEx.eval_string template, [contact: %{name: "koko", age: 99}, solution: %{age: 99}]
"Hello koko, how are you. It looks like your age is 99. So the solution of your problem is 99."
5 Likes

The problem as it’s stated is kinda ambiguous, because it’s unclear whether @contact.name whould be threated as a whole variable, or as @contact variable followed by a regulat dot. In any case, this ambuiguity might be easily resolved using the code below as a scaffold. For instance, you might have a dedicated clause for ".", check binding there, and decide whether to move forward, or use the binding.

Erlang, and hence Elixir are extremely powerful in parsing text, thanks to pattern matching.

defmodule NaiveParser do
  @varname ["." | Enum.map(?a..?z, &<<&1>>)]
  def parse(input, binding),
    do: do_parse(input, binding, {nil, ""})

  defp do_parse("", binding, {var, result}), do: result <> bound(var, binding)
  defp do_parse("@" <> rest, binding, {nil, result}),
    do: do_parse(rest, binding, {"", result})
  defp do_parse(<<c::binary-size(1), rest::binary>>, binding, {nil, result}),
    do: do_parse(rest, binding, {nil, result <> c})
  defp do_parse(<<c::binary-size(1), rest::binary>>, binding, {var, result})
      when c in @varname,
    do: do_parse(rest, binding, {var <> c, result})
  defp do_parse(<<c::binary-size(1), rest::binary>>, binding, {var, result}),
    do: do_parse(rest, binding, {nil, result <> bound(var, binding) <> c})

  defp bound(nil, binding), do: ""
  defp bound(var, binding) do
    with {substitution, ^binding} <- Code.eval_string(var, binding),
      do: to_string(substitution)
  end
end

str = "Hello @contact.name, how are you. It looks like your age is @contact.age, so the solution of your problem is @solution"
NaiveParser.parse(str, [contact: %{age: 27, name: "Rahul"}, solution: "None"])

#⇒ "Hello Rahul, how are you. It looks like your age is 27, so the solution of your problem is None"
5 Likes

If you want peak performance and slightly harder ergonomics, IO lists (aka IO data) are very hard to beat, they’re how Phoenix views are so fast.

https://hexdocs.pm/elixir/v1.10/%20/IO.html

3 Likes

I also wrote some time ago a post on the subject, perhaps there’s something useful there too

This looks similar to how gettext handles substitution.

Thank you so much everyone.

For now I am going with this code (took some input from all of you)

defmodule Glific.Flows.MessageVarParser do

@spec parse(String.t(), map()) :: String.t() | nil
    def parse(input, binding) do
    String.replace(input, ~r/@[\w]+[\.][\w]+[\.][\w]*/, &bound(&1, binding))
     |> String.replace(~r/@[\w]+[\.][\w]*/, &bound(&1, binding))
    end

    @spec bound(String.t(), map()) :: String.t()
    defp bound(nil, _binding), do: ""

    defp bound(<<_::binary-size(1), var::binary>>, binding) do
    substitution = get_in(binding, String.split(var, "."))
       if substitution == nil, do: "@#{var}", else: substitution
    end
end

I will see if I can improve this further.

2 Likes