defmodule BibleEx.RefParser do
@moduledoc """
Parses any general string for Bible references.
"""
alias BibleEx.BibleData
alias BibleEx.Reference
@doc ~S"""
use the parse_references() function to retrieve all the Bible references found in a string.
## Examples
iex> alias BibleEx.RefParser
iex> RefParser.parse_references("John 3:16")
[
%BibleEx.Reference{
book: "John",
book_names: %{abbr: "JOH", name: "John", osis: "John", short: "Jn"},
book_number: 43,
reference: "John 3:16",
reference_type: :verse,
...
}
]
iex> RefParser.parse_references("I hope Matt 2:4 and James 5:1-5 get parsed")
[
%BibleEx.Reference{
book: "Matthew",
book_names: %{abbr: "MAT", name: "Matthew", osis: "Matt", short: "Mt"},
book_number: 40,
reference: "Matthew 2:4",
reference_type: :verse,
...
},
%BibleEx.Reference{
book: "James",
book_names: %{abbr: "JAM", name: "James", osis: "Jas", short: "Jas"},
book_number: 59,
reference: "James 5:1-5",
reference_type: :verse_range,
...
}
]
iex> RefParser.parse_references("This sentence contains two references. One that spans chapters, John 3:16-4:3, found in the book of John, and another one in the same book.")
[
%BibleEx.Reference{
book: "John",
book_names: %{abbr: "JOH", name: "John", osis: "John", short: "Jn"},
book_number: 43,
reference: "John 3:16 - 4:3",
reference_type: :chapter_range,
...
},
%BibleEx.Reference{
book: "John",
book_names: %{abbr: "JOH", name: "John", osis: "John", short: "Jn"},
book_number: 43,
reference: "John",
reference_type: :chapter_range,
...
}
]
"""
def parse_references(string) when is_binary(string) do
books_matched =
Regex.scan(full_regex(), string)
|> Enum.map(fn x ->
x
end)
Enum.map(books_matched, fn x ->
cond do
# book name only i.e. ["genesis ", "genesis"]
length(x) == 2 and String.trim(Enum.at(x, 0)) == Enum.at(x, 1) ->
Reference.new(book: String.trim(Enum.at(x, 0)))
# book with chapter only i.e. ["Judges 19", "Judges", "19"]
length(x) == 3 ->
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: nil,
end_chapter: nil,
end_verse: nil
)
# book with chapter and verse i.e. ["Jn 3:16", "Jn", "3", "16"]
# also covers chapter-only edge case where user enters `James 1 - 2` i.e. ["James 1 - 2", "James", "1", "2"]
# NOTE: in elixir `James 1 - 2` generates ["james 1 - 2", "james", "1", "", "2"]
length(x) == 4 ->
# book with chapter and verse i.e. ["Jn 3:16", "Jn", "3", "16"]
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: String.to_integer(Enum.at(x, 3)),
end_chapter: nil,
end_verse: nil
)
# John 4:5-10
# iOS ["John 4:5-10", "John", "4", "5", "10"]
# elixir ["John 4:5-10", "John", "4", "5", "10"]
length(x) == 5 ->
ref = String.trim(Enum.at(x, 0))
# chapter-only reference -- also covers `em-dash` case
# # NOTE: in elixir `James 1 - 2` generates ["james 1 - 2", "james", "1", "", "2"]
cond do
String.contains?(ref, ":") ->
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: String.to_integer(Enum.at(x, 3)),
end_chapter: nil,
end_verse: String.to_integer(Enum.at(x, 4))
)
String.contains?(ref, ".") ->
# ["James 1.2 - 2", "James", "1", "2", "2"]
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: String.to_integer(Enum.at(x, 3)),
end_chapter: nil,
end_verse: nil
)
String.contains?(ref, "-") or String.contains?(ref, "—") ->
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: nil,
end_chapter: String.to_integer(Enum.at(x, 4)),
end_verse: nil
)
true ->
# should never get here?
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: String.to_integer(Enum.at(x, 3)),
end_chapter: nil,
end_verse: String.to_integer(Enum.at(x, 4))
)
end
length(x) == 6 ->
# A reference that spans multiple chapters. i.e. ["John 3:16-4:3", "John", "3", "16", "4", "3"]
Reference.new(
book: String.trim(Enum.at(x, 1)),
start_chapter: String.to_integer(Enum.at(x, 2)),
start_verse: String.to_integer(Enum.at(x, 3)),
end_chapter: String.to_integer(Enum.at(x, 4)),
end_verse: String.to_integer(Enum.at(x, 5))
)
true ->
nil
end
end)
end
defp full_regex() do
all_book_names =
Enum.map(BibleData.book_names(), fn x ->
x
end)
|> List.flatten()
variants = Map.keys(BibleData.variants())
all_searchable = all_book_names ++ variants
reg_books = Enum.join(all_searchable, "\\b|\\b")
{:ok, regex} =
Regex.compile("(\\b#{reg_books}\\b) *(\\d+)?[ :.]*(\\d+)?[— -]*(\\d+)?[ :.]*(\\d+)?", [
:caseless
])
regex
end
end