roganjoshua

roganjoshua

Guards or pattern matching processing a text file - =~

I am trying to process a text file like a kind of plain text guitar tab with etc. a bit like the Ultimate Guitar Tab site.
Each line will either blanks or chords or lyrics or something like that.

defmodule TextFileProcessor do
  def process_file(file_path) do
    File.stream!(file_path)
    |> Enum.each(&process_line/1)
  end

  defp process_line(line) when line =~ ~r/^LYRICS:/ do
    # Process lines starting with "LYRICS:"
    IO.puts("Processing lyrics line: #{line}")
  end

  defp process_line(line) when line =~ ~r/^CHORDS:/ do
    # Process lines starting with "CHORDS:"
    IO.puts("Processing chords line: #{line}")
  end

  defp process_line(line) do
    # Default processing for other lines
    IO.puts("Processing line: #{line}")
  end
end

I can’t use =~ in a guard or other Kernel functions is there a better way to do this?

I hope there is enough in here.

TIA

Marked As Solved

derpycoder

derpycoder

Here’s my take on it:

"""
LYRICS: L1
CHORDS: C1
LYRICS: L2
CHORDS: C2
CHORDS: C3
CHORDS: C4
CHORDS: C5
CHORDS: C6
TEMPO: T1
foobar\
"""
|> String.split("\n")
|> Enum.map_reduce(%{}, fn line, acc ->
  hd = line |> String.split(":") |> hd()

  {line, if(hd in ["LYRICS", "CHORDS", "TEMPO"]) do
      Map.update(acc, hd, [line], fn arr -> [line | arr] end)
  else
      Map.update(acc, "REST", [line], fn arr -> [line | arr] end)
  end}
end)
|> then(fn {_lines, acc} -> acc end)

or if you don’t care about specifics in the first iteration, you can do:

"""
LYRICS: L1
CHORDS: C1
LYRICS: L2
CHORDS: C2
CHORDS: C3
CHORDS: C4
CHORDS: C5
CHORDS: C6
TEMPO: T1
foobar\
"""
|> String.split("\n")
|> Enum.map_reduce(%{}, fn line, acc ->
  hd = line |> String.split(":") |> hd() || "REST"

  {line, Map.update(acc, hd, [line], fn arr -> [line | arr] end)}
end)
|> then(fn {_lines, acc} -> acc end)

Both the pipelines, spit out an organized map, which you can then process.

%{
  "CHORDS" => ["CHORDS: C6", "CHORDS: C5", "CHORDS: C4", "CHORDS: C3", "CHORDS: C2", "CHORDS: C1"],
  "LYRICS" => ["LYRICS: L2", "LYRICS: L1"],
  "REST" => ["foobar"],
  "TEMPO" => ["TEMPO: T1"]
}

If you don’t want that map, you can directly process it within the pipeline above.


P.S.

  1. Instead of split, you can use regex.
  2. Instead of if-else, you can use cond.

Personally, I prefer this way because it makes it composable and allows me to use dbg() to see if anything in the pipeline is not working as expected.

Also Liked

dimitarvp

dimitarvp

I am pretty late here but I’d advise against regexes unless they are very straightforward. Haven’t looked into yours in details (and length is not always an indicator of a complex regex) but I’d probably reach for nimble_parsec and give it a go for a day or two.

That being said, being productive in that particular library is a skill in and of itself so if you are pressed for time you should probably keep your regex solution but also add edge case unit tests.

christhekeele

christhekeele

Not in guards/function heads. You’ll need to use a single function with something like a cond inside.

roganjoshua

roganjoshua

If anyone is interested I think I kind of got to where I wanted like this:

defmodule Textprocessor do
  @regex_find_chords ~r/\b(?:[BE]b?|[ACDFG]#?)(?:sus|m|maj|min|[-1-9\/m])?(?:sus|m|maj|min|[-1-9\/m])?\b#?/
  @empty_line ~r/^\s*$/
  @instruction ~r/^\[[\w+ ]+\]\s*$/

  @moduledoc """
  Documentation for `Textprocessor`.
  """

  @doc """
  Process song

  ## Examples
    # iex> Textprocessor.process_file("./lyrics.text")
    # :ok
  """

  def process_file(filename) do
    File.stream!(filename)
    |> Enum.map(&String.trim/1)
    |> Enum.each(&process_line/1)
  end

  defp process_line(line) do
    cond do
      String.match?(line, @regex_find_chords) ->
        IO.puts("CHORDS: #{line}")

      String.match?(line, @instruction) ->
        IO.puts("INSTRUCTION: #{line}")

      String.match?(line, @empty_line) ->
        IO.puts("EMPTY: #{line}")

      true ->
        IO.puts("LYRICS: #{line}")
        :ok
    end
  end
end

Not sure this is optimal but I would be interested in any criticism

Marcus

Marcus

You can do this with pattern matching in the function args.

defmodule TextProcessor do
  def process_string(string) do
    string
    |> String.split("\n")
    |> Enum.each(&process_line/1)
  end

  defp process_line("LYRICS:" <> rest) do 
    # Process lines starting with "LYRICS:"
    IO.puts("Processing lyrics line: #{rest}")
  end

  defp process_line("CHORDS:" <> rest) do 
    # Process lines starting with "CHORDS:"
    IO.puts("Processing chords line: #{rest}")
  end

  defp process_line(line) do
    # Default processing for other lines
    IO.puts("Processing line: #{line}")
  end
end

TextProcessor.process_string("""
LYRICS: foo 
CHORDS: bar
foobar\
""")
Processing lyrics line:  foo 
Processing chords line:  bar
Processing line: foobar
LostKobrakai

LostKobrakai

There are constraints around guards, which prevent many higher level/more complex apis from being usable:

Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently.

https://hexdocs.pm/elixir/patterns-and-guards.html

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New

We're in Beta

About us Mission Statement