defmodule BibleEx.Reference do @moduledoc """ Represents a normalized Bible reference. A `%BibleEx.Reference{}` ties together book, chapter(s), verse(s), and derived metadata such as OSIS code, abbreviation, and the list of `BibleEx.Chapter` and `BibleEx.Verse` structs that make up the reference. """ alias BibleEx.Librarian alias BibleEx.Chapter alias BibleEx.Verse @enforce_keys [:book] defstruct book: "", book_names: %{}, book_number: nil, reference: nil, reference_type: nil, start_chapter: nil, start_chapter_number: nil, start_verse: nil, start_verse_number: nil, end_chapter: nil, end_chapter_number: nil, end_verse: nil, end_verse_number: nil, is_valid: false, chapters: [], verses: [], osis: nil, abbr: nil, short: nil @doc ~S""" Builds a new `%BibleEx.Reference{}` for a given book and optional range. The `book` parameter may be any recognized form for a book name: * Full name: `"Genesis"` * OSIS code: `"Gen"` * Paratext abbreviation: `"GEN"` * Short form: `"Gn"` Missing chapter or verse boundaries are filled in using `BibleEx.Librarian`, defaulting to the full book, chapter, or verse range as appropriate. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.new(book: "Genesis", start_chapter: 2, start_verse: 3, end_chapter: 4, end_verse: 5) iex> ref.book "Genesis" iex> {ref.start_chapter_number, ref.start_verse_number, ref.end_chapter_number, ref.end_verse_number} {2, 3, 4, 5} iex> ref.reference_type in [:chapter_range, :verse_range] true iex> alias BibleEx.Reference iex> ref = Reference.new(book: "3 John") iex> ref.book "3 John" iex> {ref.start_chapter_number, ref.start_verse_number} {1, 1} iex> ref.is_valid true """ def new(book: book) do new(book: book, start_chapter: nil, start_verse: nil, end_chapter: nil, end_verse: nil) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a book starting at a given chapter. This is a convenience wrapper for `new/1` when only `start_chapter` is known. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.new(book: "Genesis", start_chapter: 1) iex> {ref.book, ref.start_chapter_number, ref.start_verse_number} {"Genesis", 1, 1} """ def new( book: book, start_chapter: start_chapter ) do new( book: book, start_chapter: start_chapter, start_verse: nil, end_chapter: nil, end_verse: nil ) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a single starting verse. This is a convenience wrapper for `new/1` when `book`, `start_chapter` and `start_verse` are known but no end boundary is supplied. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.new(book: "Gen", start_chapter: 1, start_verse: 1) iex> ref.reference "Genesis 1:1" """ def new( book: book, start_chapter: start_chapter, start_verse: start_verse ) do new( book: book, start_chapter: start_chapter, start_verse: start_verse, end_chapter: nil, end_verse: nil ) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a cross-chapter range without an end verse. This form is used when the start verse is known but the range ends at the end of `end_chapter`. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.new(book: "John", start_chapter: 3, start_verse: 16, end_chapter: 4) iex> {ref.start_chapter_number, ref.start_verse_number, ref.end_chapter_number} {3, 16, 4} """ def new( book: book, start_chapter: start_chapter, start_verse: start_verse, end_chapter: end_chapter ) do new( book: book, start_chapter: start_chapter, start_verse: start_verse, end_chapter: end_chapter, end_verse: nil ) end @doc ~S""" Builds the most general `%BibleEx.Reference{}` with optional start and end. This function: * Normalizes the book name using `BibleEx.Librarian.get_book_names/1`. * Computes default start and end chapters/verses when omitted. * Loads `BibleEx.Chapter` and `BibleEx.Verse` structs for the range. * Sets `reference`, `reference_type`, and `is_valid` using `BibleEx.Librarian`. Prefer calling the convenience helpers (`chapter/2`, `verse/3`, `chapter_range/3`, `verse_range/4`) instead of this function directly in most application code. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.new(book: "Romans", start_chapter: 8, start_verse: 28, end_chapter: 8, end_verse: 30) iex> ref.reference "Romans 8:28-30" iex> ref.is_valid true """ def new( book: book, start_chapter: start_chapter, start_verse: start_verse, end_chapter: end_chapter, end_verse: end_verse ) do bname = case Librarian.get_book_names(book: book) do book_names = %{ osis: _osis, abbr: _abbr, name: _name, short: _short } -> Map.get(book_names, :name, book) %{} -> book end sc = if !is_nil(start_chapter) do Chapter.new(book: bname, chapter_number: start_chapter) else Chapter.new(book: bname, chapter_number: 1) end scn = if !is_nil(start_chapter) do start_chapter else 1 end ec = if !is_nil(end_chapter) do Chapter.new(book: bname, chapter_number: end_chapter) else if !is_nil(start_chapter) do Chapter.new(book: bname, chapter_number: start_chapter) else Chapter.new(book: bname, chapter_number: end_chapter) end end ecn = if !is_nil(end_chapter) do end_chapter else if !is_nil(start_chapter) do scn else Librarian.get_last_chapter_number(book: bname) end end sv = if !is_nil(start_verse) do Verse.new(book: bname, chapter_number: start_chapter, verse_number: start_verse) else Verse.new(book: bname, chapter_number: 1, verse_number: 1) end svn = if !is_nil(start_verse) do start_verse else 1 end ev = if !is_nil(end_verse) do Verse.new(book: bname, chapter_number: scn, verse_number: end_verse) else if !is_nil(start_verse) do Verse.new(book: bname, chapter_number: scn, verse_number: start_verse) else Librarian.get_last_verse_number(book: bname, chapter: scn) end end evn = if !is_nil(end_verse) do end_verse else if !is_nil(start_verse) do start_verse else Librarian.get_last_verse_number(book: bname, chapter: ecn) end end chapters = Librarian.get_chapters( book: bname, start_chapter: scn, end_chapter: ecn ) chapter_list = scn..ecn |> Enum.to_list() verse_collection = Enum.map(chapter_list, fn x -> find_start_verse = if !is_nil(svn) and x == Enum.at(chapter_list, 0) do svn else 1 end find_last_verse = if !is_nil(evn) and x == List.last(chapter_list) do evn else Librarian.get_last_verse_number(book: bname, chapter: x) end Librarian.get_verses( book: bname, chapter: x, start_verse: find_start_verse, end_verse: find_last_verse ) end) book_names = Librarian.get_book_names(book: bname) osis_book = Map.get(book_names, :osis, nil) abbr_book = Map.get(book_names, :abbr, nil) short_book = Map.get(book_names, :short, nil) %__MODULE__{ book: bname, book_names: book_names, book_number: Librarian.find_book_number(book: bname), reference: Librarian.create_reference_string( book: bname, start_chapter: start_chapter, start_verse: start_verse, end_chapter: ecn, end_verse: end_verse ), reference_type: Librarian.identify_reference_type( book: bname, start_chapter: scn, start_verse: start_verse, end_chapter: ecn, end_verse: end_verse ), start_chapter: sc, start_chapter_number: scn, end_chapter: ec, end_chapter_number: ecn, start_verse: sv, start_verse_number: svn, end_verse: ev, end_verse_number: evn, is_valid: Librarian.verify_reference( book: bname, start_chapter: scn, start_verse: start_verse, end_chapter: ecn, end_verse: end_verse ), chapters: chapters, verses: List.flatten(verse_collection), osis: osis_book, abbr: abbr_book, short: short_book } end @doc ~S""" Builds a `%BibleEx.Reference{}` for a whole chapter. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.chapter(book: "Genesis", chapter: 1) iex> {ref.book, ref.reference_type} {"Genesis", :chapter} iex> ref.reference "Genesis 1" """ def chapter(book: book, chapter: chapter) do new(book: book, start_chapter: chapter) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a single verse. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.verse(book: "Genesis", chapter: 1, verse: 1) iex> {ref.book, ref.reference_type} {"Genesis", :verse} iex> ref.reference "Genesis 1:1" """ def verse(book: book, chapter: chapter, verse: verse) do new(book: book, start_chapter: chapter, start_verse: verse) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a chapter range. The range spans from `start_chapter` to `end_chapter`, inclusive. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.chapter_range(book: "Genesis", start_chapter: 1, end_chapter: 2) iex> ref.reference "Genesis 1-2" iex> ref.reference_type :chapter_range """ def chapter_range(book: book, start_chapter: start_chapter, end_chapter: end_chapter) do new( book: book, start_chapter: start_chapter, start_verse: nil, end_chapter: end_chapter, end_verse: nil ) end @doc ~S""" Builds a `%BibleEx.Reference{}` for a verse range within a chapter. ## Examples iex> alias BibleEx.Reference iex> ref = Reference.verse_range(book: "Matt", chapter: 2, start_verse: 4, end_verse: 10) iex> ref.book "Matthew" iex> {ref.start_verse_number, ref.end_verse_number} {4, 10} iex> ref.reference_type :verse_range """ def verse_range(book: book, chapter: chapter, start_verse: start_verse, end_verse: end_verse) do new( book: book, start_chapter: chapter, start_verse: start_verse, end_chapter: nil, end_verse: end_verse ) end end