I stuck with this text-file manipulation problem

i stuck with this problem,

Screenshot 2021-07-31 at 5.25.29 PM

i could read the text from text-file and index it but i could not update
I’m missing something

defmodule ReadText do
  def read_text_file(file_name) do
    case File.read(file_name) do
      {:ok, text} ->
        IO.puts("slice 1: #{String.slice(text, 3..8)}")
        slice1 = IO.gets("slice 1 to replace: ")
        String.replace(text, String.slice(text, 3..8), slice1, global: false)
        IO.puts("slice 2: #{String.slice(text, 72..80)}")
        slice2 = IO.gets("slice 2 to replace: ")
        String.replace(text, String.slice(text, 72..80), slice2, global: false)
        IO.puts("slice 3: #{String.slice(text, 86..91)}")
        slice3 = IO.gets("slice 3 to replace: ")
        String.replace(text, String.slice(text, 86..91), slice3, global: false)
        IO.puts("slice 4: #{String.slice(text, 101..110)}")
        slice4 = IO.gets("slice 4 to replace: ")
        String.replace(text, String.slice(text, 101..110), slice4, global: false)

      {:error, error} ->
        IO.puts(error)
    end
  end

  def get_replaceable_indexs(file_name) do
    case File.read(file_name) do
      {:ok, text} ->
        text
        |> String.split("")
        |> Enum.with_index(fn v, i -> [i, v] end)

      {:error, error} ->
        IO.puts(error)
    end
  end

  def open(file_path) do
    File.open(file_path, [:read, :write], fn text ->
      IO.read(text, :all)
    end)
  end
end

i tried in livebook to get dynamic index for square brockets and use this index as rang in String.slice(text, 3..8) but i’m missing the logic

i highly appreciate any help
thank you

Values in Elixir are immutable (they cannot be changed once created) - functions like String.replace return a new binary.

You can rebind variables, however. For instance,

  text = String.replace(text, String.slice(text, 72..80), slice2, global: false

After this line, the name text will refer to the result of String.replace instead of the original input.


General note: hardcoding numerical offsets that are passed to String.slice is very likely not what the problem is really looking for. Take a look at the Regex module for a better way to find sequences like “left square bracket followed by letters followed by right square bracket” and manipulate them.

7 Likes

Using string slices is absolutely not what you want here. I’d tell you that you failed the interview if you showed that to me.

Look for ways to search [anything] in the source text and replace that. Regex is a good start and might even be good enough as a final solution.

4 Likes

Yeah I know hard coding numbers are not the proper solution here and I have theoretical solution but struggling to implement that,

I’m a newbie getting into programming and trying my share of struggles, I’ll learn as I practice,
I’ll try regex functions

If I understand the problem correctly you need to replace variable placeholders like [name] with passed in arguments. You can use a Regex to solve this but if the structure of source.txt is exactly as it appears you can also use Elixir binary pattern matching. You can recurse over a binary file and match patterns like [name] [company] [time] [salesguy].

To keep things simple you can pass in a map as an argument

  %{name: "John", company: "Google", time: "3:30pm", salesguy: "Ralph"},

Now with binary pattern matching you can use this map to replace the placeholder variables.

defmodule Replacer do

  defp template do
    Application.app_dir(:replacer, "/priv/source.txt")
  end

  def replace_text(sample_data) when is_map(sample_data) do
    template()
    |> File.read!()
    |> replace(sample_data)
  end

  defp replace(source, sample_data) do
    replace(source, sample_data, [])
  end

  defp replace("", _sample_data, acc) do
    acc
    |> Enum.reverse()
    |> IO.iodata_to_binary()
  end

  defp replace(<<"[name]", rest::binary>>, sample_data, acc) do
    name = sample_data.name
    replace(rest, sample_data, [name | acc])
  end

  defp replace(<<"[company]", rest::binary>>, sample_data, acc) do
    company = sample_data.company
    replace(rest, sample_data, [company | acc])
  end

  defp replace(<<"[time]",  rest::binary>>, sample_data, acc) do
    time = sample_data.time
    replace(rest, sample_data, [time | acc])
  end

  defp replace(<<"[salesguy]",  rest::binary>>, sample_data, acc) do
    salesguy = sample_data.salesguy
    replace(rest, sample_data, [salesguy | acc])
  end

  defp replace(<<head, rest::binary>>, sample_data, acc) do
    replace(rest, sample_data, [head | acc])
  end

end

Now in iex you can

iex(1)>  sample_data = %{name: "John", company: "Google", time: "3:30pm", salesguy: "Ralph"}
iex(2)> iex(2)> Replacer.replace_text(record)
"Hi John,\nThank you for your time in our office.\nThanks for booking at Google for 3:30pm\nRegards\nRalph\n"
2 Likes

Thank you @ericgray it works and I’m trying on regex implementation,
still reading different regex and string related doc’s, articles

2 Likes

Great that’s a good way to learn. Try different things to see what works best for you. Regex patterns are good but they can be cryptic and hard to read. I think in this case where you know the shape of the data before hand binary pattern matching is easier in my opinion. Try a Regex and let us know what you come up with.

3 Likes

Suppose you have a map that stores the attributes you want to stuff into the template, like

attrs = %{
  "name" => "Charlie Bucket",
  "company" => "The Chocolate Factory",
  "time" => "Aug 12, 2021",
  "salesguy" => "Willy Wonka"
}

you can try

# `source` is the content read from `source.txt`
result =
  Enum.reduce(attrs, source, fn {key, value}, acc ->
    String.replace(acc, "[#{key}]", value, global: true)
  end)

or

result = 
  for {key, value} <- attrs, reduce: source do
    acc -> String.replace(acc, "[#{key}]", value, global: true)
  end
2 Likes

Given attrs:

attrs = %{
  "name" => "Charlie Bucket",
  "company" => "The Chocolate Factory",
  "time" => "Aug 12, 2021",
  "salesguy" => "Willy Wonka"
}

and an input string in source, a single call to Regex.replace can do this:

Regex.replace(~r/\[([^\]]+)\]/, source, fn _, key -> attrs[key] end)

This will silently replace unrecognized keys with empty strings; use something like Map.fetch if that isn’t desired.

The regex here looks worse than it is, because square brackets are metacharacters in regex:

  • \[ matches a literal open bracket
  • ([^\]]+) captures one or more characters that aren’t a ]
  • \] matches a literal close bracket
2 Likes

That’s faster than my solution, I guess, since it goes through the template string only once.

If the keys contain only word characters (i.e. a to z, A to Z, numbers, and _), the regex can be simplified as

~r/\[(\w+)\]/

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 Likes

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

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 Like

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 Like

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 Like

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://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 Likes

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