Aetherus

Aetherus

And we can do something crazy (for practicing tail recursion, of course. You don’t wanna use this sorta code in production).

The following code assumes there’s no mismatching bracket, and there’s no nested bracket. The code is NOT unicode-safe.

defmodule MyTemplate do

  @spec consolidate(String.t, %{optional(String.t) => String.t}) :: String.t
  def consolidate(template, replacements) do
    do_consolidate(template, replacements, [], nil)
  end

  # I know @doc has no effect on private functions,
  # but it looks better to write docs in this way.
  @doc """
  Handle the consolidation progress one character at a time.

  ## Params

    - `template`: the yet-to-handle part of the template string.
    - `replacements`: the map of replacements.
    - `acc`: an IO list. The accumulator of the content generated so far.
    - `placeholder`: `nil` or an IO list.
             If `placeholder` is `nil`, it means this function is handling a character out of any brackets,
             otherwise, this function is handling a character inside a bracket.

  ## Return value

    A string with all placeholders replaced with their corresponding values in `replacements`.
  """
  defp do_consolidate("[" <> rest, replacements, acc, nil) do
    # Encountered an open bracket outside any brackets.
    # The next few characters should be a placeholder,
    # so recurse with an empty IO list to store the placeholder.
    do_consolidate(rest, replacements, acc, [])
  end

  defp do_consolidate("]" <> rest, replacements, acc, placeholder) do
    # Encountered a close bracket.
    # `placeholder` should contain all the characters of a placeholder,
    # so we lookup the value in `replacements` and append it to `acc`.

    placeholder = IO.iodata_to_binary(placeholder)
    value = replacements[placeholder] || ""
    do_consolidate(rest, replacements, [acc, value], nil)
  end

  defp do_consolidate(<<char::binary-1, rest::binary>>, replacements, acc, nil) do
    # Encountered a non-bracket character outside any brackets.
    # Just append the character to `acc`.
    do_consolidate(rest, replacements, [acc, char], nil)
  end

  defp do_consolidate(<<char::binary-1, rest::binary>>, replacements, acc, placeholder) do
    # Encountered a non-bracket character inside a bracket.
    # This character should be part of a placeholder,
    # so we append this character to `placeholder`.
    do_consolidate(rest, replacements, acc, [placeholder, char])
  end

  defp do_consolidate("", _replacements, acc, nil) do
    # The whole template is handled.
    # Just convert `acc` to a string and return it.
    IO.iodata_to_binary(acc)
  end
end

You can try call it:

template = """
Hi [name],
Thank you for your time in our office.

Thank you for booking at [company] for [time].

Regards
[salesguy]
"""

template
|> MyTemplate.consolidate(%{
  "company" => "The Chocolate Factory",
  "name" => "Charlie Bucket",
  "salesguy" => "Willy Wonka",
  "time" => "Aug 12, 2021"
})
|> IO.puts()
2
Post #11
ambareesha7

ambareesha7 OP

I’m going with this approach for now but still I’m looking for better and easy solution

0
Post #12
ambareesha7

ambareesha7 OP

i spoke to one of my known senior developer he given me this solution:
read_file.exs its a script file

defmodule ReadFiles do @output_file "output.txt" def read_file(args) do case File.read("source.txt") do {:ok, body} -> body |> ReadFiles.replace_variables(parse_args(args)) |> ReadFiles.write_to_file() {:error, reason} -> IO.puts "Error reading file. Reason: #{inspect reason}" end end 
def replace_variables(body, vars) do
	body
	|> String.split("\n")
	|> Enum.map(&replace_line(&1, vars))
	|> Enum.join("\n")
end

def replace_line(line, vars) do
	String.split(line, " ")
	|> Enum.map(fn a ->
		replace_word(a, vars)
	end)
	|> Enum.join(" ")
end

defp replace_word(word, vars) do
	case String.match?(word, ~r/\[\w+\]/) do
		true ->
			[prefix, var_name, suffix] = String.split(word, ["[", "]"])
			replacement = Map.get(vars, var_name)
			Enum.join([prefix, replacement, suffix], "")
		false ->
			word
	end
end

def write_to_file(contents) do
	case File.write(@output_file, contents) do
		:ok ->
			IO.puts "Wrote file: #{@output_file}.\n"
		_ ->
			IO.puts "Failed to write file."
	end
end

defp parse_args(args) do
	args |> Enum.map(&String.split(&1, "="))
	|> Enum.reduce(%{}, fn [k|v], acc -> Map.put(acc, k, v) end)
end

end

System.argv() |> ReadFiles.read_file()

 and run the code like this: 
elixir read_file.exs name=John company=Google time=3:30pm salesguy=Ralph
this command will throwout a text file named "output.txt" with modified changes

i really love elixir we can come up with different solutions as we pleased

1
Post #13
Aetherus

Aetherus

And let’s do something even crazier: Metaprogramming!

Suppose you have a file (say, placeholders.txt) that lists all possible placeholders, like this:

name
company
time
salesguy

you can define the MyTemplate module like this:

defmodule MyTemplate do
  @placeholder_file_path "placeholders.txt"

  # Uncomment this if you want to leverage live recompile when the content of placeholder.txt changed
  # @external_resource @placeholder_file_path

  def consolidate(template, replacements) do
    do_consolidate(template, replacements, [])
  end

  # For each placeholder listed in the placeholders.txt,
  # define a function clause like:
  # 
  #     defp do_consolidate("[name]" <> rest, replacements, acc) do
  #       do_consolidate(rest, replacements, [acc, replacements["name"] || ""])
  #     end
  # 
  for placeholder <- File.stream!(@placeholder_file_path) |> Enum.map(&String.trim/1) do
    defp do_consolidate(unquote("[#{placeholder}]") <> rest, replacements, acc) do
      do_consolidate(rest, replacements, [acc, replacements[unquote(placeholder)] || ""])
    end
  end

  defp do_consolidate(<<char::binary-1, rest::binary>>, replacements, acc) do
    do_consolidate(rest, replacements, [acc, char])
  end

  defp do_consolidate("", _replacements, acc) do
    IO.iodata_to_binary(acc)
  end
end
1
Post #14
madlep

madlep

Had a play with this.

We can get even more funky using defmacro to add some compile time optimisation so the template is only ever parse once. Then we just assemble an iolist by transforming only the [param] values, leaving the rest of the string alone, and passing it out through IO.iodata_to_binary/1 to generate the string (and if you were using it directly over a socket etc, you could even skip that).

This is kinda similar to how Phoenix renders templates - don’t parse a massive string every time, just figure out a representation with the separate parts once, then replace variables at runtime, and then concat output strings into an iolist.

defmodule Template do
  defmacro template(path) do
    # load and parse template *once only*, at compile time :)
    parts =
      path
      |> File.read!()
      |> parse()

    quote do
      # recompile template module this is run in when template text changes
      @external_resource unquote(path)

      # generate function render/1 function in template module with parsed parts baked in
      def render(params) do
        unquote(parts)
        |> Template.render(params)
      end
    end
  end

  def parse(template) do
    # split template string on "[param_name]" blocks. `U` flag means "ungreedy",
    # so it will only consume ONE param, not look for the largest string between
    # the start and end of multiple [] params.
    ~r"\[.*\]"U
    # `include_captures` keeps the [param_name] blocks for us
    |> Regex.split(template, include_captures: true)
    # we get a list of plain string parts, and "[param_name]" strings. Turn the
    # param name strings into tuple so we can distinguish them later when
    # rendering.
    |> Enum.map(fn part ->
      case Regex.run(~r"\[(?<part>.*)\]", part, capture: ["part"]) do
        [param_name] -> {:param, String.to_atom(param_name)}
        nil -> part
      end
    end)

    # parsed template looks something like:
    # [
    #   "Hi ",
    #   {:param, :name},
    #   ",\nThank you for your time in our office.\n\nThanks for booking at ",
    #   {:param, :company},
    #   " for ",
    #   {:param, :time},
    #   "\n\nRegards\n",
    #   {:param, :salesperson},
    #   "\n"
    # ]
  end

  def render(parts, params) do
    # rendering is super efficient, just replacing {:param, :param_name} with
    # the param name looked up from the params provided to build an iolist, then
    # converting it to a binary
    parts
    |> Enum.map(&render_part(&1, params))
    |> IO.iodata_to_binary()
  end

  defp render_part({:param, param_name}, params), do: Access.fetch!(params, param_name)
  defp render_part(part, _params) when is_binary(part), do: part
end

Then to use it, we just import Template into the module that defines the template.

defmodule MyTemplate do
  import Template

  template("priv/source.txt")
end

And then call it with the params

MyTemplate.render(
  name: "Bob",
  company: "Awesome McAwesome Co",
  time: "3:00pm",
  salesperson: "Sally Sales"
)
1
Post #15
pdgonzalez872

pdgonzalez872

Seems like calling String.replace/3 solves this nicely? Maybe I’m missing something.

ExUnit.start()

defmodule Test do
  use ExUnit.Case

  describe "implementation for https://forum.elixirforum.com/t/i-stuck-with-this-text-file-manipulation-problem/41384" do

    defmodule ReplaceVariablesImplementation do
      def call(text, %{name: name, company: company, time: time, salesguy: salesguy} = _args) do
        text
        |> String.replace("[name]", name)
        |> String.replace("[company]", company)
        |> String.replace("[time]", time)
        |> String.replace("[salesguy]", salesguy)
      end
    end

    test "Replaces variables in text" do
      # Do File.read!/1 to get the file contents. Using a variable for brevity.
      text = """
      Hi [name],
      Thank you for your time in our office.

      Thank you for booking at [company] for [time].

      Regards
      [salesguy]
      """

      # Using a string for time for brevity, maybe this is your use case, maybe not.
      args = %{name: "Jane", company: "", time: "2022/3/25 13:00", salesguy: "Joe"}

      expected = """
      Hi Jane,
      Thank you for your time in our office.

      Thank you for booking at  for 2022/3/25 13:00.

      Regards
      Joe
      """

      result = ReplaceVariablesImplementation.call(text, args)

      assert result == expected
    end
  end
end

(the test passes)

3
Post #16
ambareesha7

ambareesha7 OP

Your code does the job
Actually I was overthinking on that issue, that’s why I posted it here but after I solved it, it was simple

0
Post #17
— All posts loaded —