ChordPro parser
by Paulo Gonzalez
2023-02-01 | elixir parsers manual-parser chordpro eddi
chordpro-parser
ChordPro parser
My brother asked me about a script that would convert chorded songs to the ChordPro format. This seems to work.
Standalone Elixir script (save it to a file and run it as `elixir file_name.exs`)
ExUnit.start()
defmodule ChordProTest do
use ExUnit.Case
defmodule ChordProParser do
@moduledoc """
Parses text from the normal chord format to the ChordPro format.
It takes an input like this:
Bm G
Some things in life are bad
C Am
They can really make you mad
Dm G C
Other things just make you swear and curse
Dm G
When you are chewing on life's gristle
C Am
Don't grumble give a whistle
D7 G7
And this'll will make things turn out for the best
And converts it to:
Some [Bm]things in life are [G]bad
They can [C]really make you [Am]mad
Other [Dm]things just make you [G]swear and [C]curse
When you are [Dm]chewing on life's [G]gristle
Don't [C]grumble give a [Am]whistle
And [D7]this'll will make things turn out for the [G7]best
More info here: https://www.chordpro.org/
"""
require Logger
@doc """
Entrypoint to the parser.
"""
def call(input) when is_binary(input) do
input
|> String.split("\n", trim: true)
|> Enum.chunk_every(2)
|> Enum.map(fn [chords, lyrics] ->
clumped_chords = clump_words(chords)
lyrics
|> String.split("", trim: true)
|> Enum.with_index()
|> Enum.reduce(" ", fn {char, index}, acc ->
result = Map.get(clumped_chords, "#{index}")
if result do
acc <> "[#{result}]" <> char
else
acc <> char
end
end)
end)
|> Enum.map(fn el -> String.trim_leading(el) end)
|> Enum.join("\n")
end
@doc """
Helps with determining which column a chord should be inserted
"""
def clump_words(chord_line) when is_binary(chord_line) do
with character_split <- String.split(chord_line, "", trim: true),
with_index <- Enum.with_index(character_split),
clumped <-
Enum.reduce(with_index, %{}, fn
{" ", _index}, acc ->
acc
{char, 0 = index}, acc ->
next_chars = get_next_char(Enum.drop(character_split, index + 1))
Map.put(acc, "#{index}", char <> next_chars)
{char, index}, acc ->
# don't process 6 if we have 5
if Enum.at(character_split, index - 1) == " " do
next_chars = get_next_char(Enum.drop(character_split, index + 1))
Map.put(acc, "#{index}", char <> next_chars)
else
acc
end
end) do
clumped
else
error ->
error |> IO.inspect(label: "Error\n\n")
end
end
defp get_next_char([]) do
""
end
defp get_next_char([" " | _rest]) do
""
end
defp get_next_char([char | rest]) do
char <> get_next_char(rest)
end
end
describe "call/1" do
test "works as expected - 2 lines, easy case" do
result =
"""
Bm G
Some things in life are bad
C Am
They can really make you mad
"""
|> ChordProParser.call()
expected = """
Some [Bm]things in life are [G]bad
They can [C]really make you [Am]mad
"""
|> String.trim()
assert result == expected
end
test "works as expected - 2 lines, easy case - eddi 1" do
result =
"""
C G F C
Más bonita que ninguna, ponía a la peña de pie
"""
|> ChordProParser.call()
expected = "[C]Más bonita que nin[G]guna, po[F]nía a la peñ[C]a de pie"
assert result == expected
end
test "works as expected - 2 lines, easy case - eddi 2" do
result =
"""
Cm G F C
Más bonita que ninguna, ponía a la peña de pie
"""
|> ChordProParser.call()
expected = "[Cm]Más bonita que nin[G]guna, po[F]nía a la peñ[C]a de pie"
assert result == expected
end
test "works as expected - example in https://www.chordpro.org/" do
input = """
Bm G
Some things in life are bad
C Am
They can really make you mad
Dm G C
Other things just make you swear and curse
Dm G
When you are chewing on life's gristle
C Am
Don't grumble give a whistle
D7 G7
And this'll will make things turn out for the best
"""
result = ChordProParser.call(input)
expected = """
Some [Bm]things in life are [G]bad
They can [C]really make you [Am]mad
Other [Dm]things just make you [G]swear and [C]curse
When you are [Dm]chewing on life's [G]gristle
Don't [C]grumble give a [Am]whistle
And [D7]this'll will make things turn out for the [G7]best
"""
|> String.trim()
assert result == expected
end
test "works as expected - eddy example" do
input = """
C G F C
Más bonita que ninguna, ponía a la peña de pie
G Am F G
Con más noches que la luna, estaba todo bien
C G F C
Probaste fortuna en 1996
G Am F G
De Málaga hasta La Coruña, durmiendo en la estación de tren
"""
result = ChordProParser.call(input)
expected = """
[C]Más bonita que nin[G]guna, po[F]nía a la peñ[C]a de pie
[G]Con más noches que la [Am]luna, [F]estaba todo[G] bien
[C]Probaste for[G]tuna en [F]19[C]96
[G]De Málaga hasta La C[Am]oruña, [F]durmiendo en la estació[G]n de tren
"""
|> String.trim()
assert result == expected
end
end
describe "clump_words/1" do
test "can clump chord lines" do
assert %{"24" => "G", "5" => "Bm"} = ChordProParser.clump_words(" Bm G")
assert %{"25" => "G", "5" => "Bm7"} =
ChordProParser.clump_words(" Bm7 G")
assert %{"19" => "Am", "3" => "C"} = ChordProParser.clump_words(" C Am")
assert %{"12" => "in", "15" => "life", "20" => "are", "24" => "bad", "5" => "things"} =
ChordProParser.clump_words("Some things in life are bad")
end
end
end
Fun exercise. I added the code to a gist -> https://gist.github.com/pdgonzalez872/fca8876a3a59bea78a30b173dbaaee95
Thanks for reading!