Skip to main content

bible_io_references/
passage.rs

1//! Rich Bible passage values and parsing.
2//!
3//! [`Reference`] represents one verse or one contiguous
4//! verse range. A [`Passage`] can additionally represent a whole book, one or
5//! more chapters, a comma-separated verse selection, or a semicolon-separated
6//! sequence of passage expressions.
7
8use core::{fmt, str::FromStr};
9
10use crate::{
11    Book, BookMatch, Language, ParseError, ParseErrorKind, ParseMetadata, Parsed, Reference,
12    ReferenceParser, VerseRange, VerseRef,
13    normalize::normalize_for_parsing,
14    reference::{MAX_CHAPTER_NUMBER, MAX_VERSE_NUMBER},
15};
16
17/// Books whose conventional bare-number notation names a verse in chapter 1.
18pub const DEFAULT_SINGLE_CHAPTER_BOOKS: [Book; 5] = [
19    Book::Obadiah,
20    Book::Philemon,
21    Book::SecondJohn,
22    Book::ThirdJohn,
23    Book::Jude,
24];
25
26/// Failure to construct a passage value without satisfying its invariants.
27#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28#[non_exhaustive]
29pub enum PassageBuildError {
30    /// A chapter coordinate was zero or exceeded the package sanity limit.
31    InvalidChapter {
32        /// The rejected chapter coordinate.
33        value: u16,
34    },
35    /// An inclusive chapter range did not have a strictly ascending end.
36    ChapterRangeNotAscending {
37        /// The first chapter in the rejected range.
38        start: u16,
39        /// The final chapter in the rejected range.
40        end: u16,
41    },
42    /// A verse passage contained no selections.
43    EmptyVersePassage,
44    /// A passage sequence contained no passages.
45    EmptyPassageSequence,
46}
47
48impl fmt::Display for PassageBuildError {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match *self {
51            Self::InvalidChapter { value } => write!(
52                formatter,
53                "chapter must be between 1 and {MAX_CHAPTER_NUMBER} (got {value})"
54            ),
55            Self::ChapterRangeNotAscending { start, end } => write!(
56                formatter,
57                "end chapter {end} must come after start chapter {start}"
58            ),
59            Self::EmptyVersePassage => {
60                formatter.write_str("a verse passage must contain at least one reference")
61            }
62            Self::EmptyPassageSequence => {
63                formatter.write_str("a passage sequence must contain at least one passage")
64            }
65        }
66    }
67}
68
69impl std::error::Error for PassageBuildError {}
70
71/// A complete Bible passage expression.
72#[derive(Clone, Debug, Eq, Hash, PartialEq)]
73pub enum Passage {
74    /// A complete book.
75    Book(BookPassage),
76    /// One chapter or an inclusive range of chapters.
77    Chapter(ChapterPassage),
78    /// One or more discrete verse references.
79    Verses(VersePassage),
80    /// A source-ordered sequence of passage expressions.
81    Sequence(PassageSequence),
82}
83
84impl Passage {
85    /// Parse a passage using automatic language detection.
86    pub fn parse(input: &str) -> Result<Self, ParseError> {
87        PassageParser::new().parse(input)
88    }
89
90    /// Parse a passage using one explicit book-name language.
91    pub fn parse_with_language(input: &str, language: Language) -> Result<Self, ParseError> {
92        PassageParser::new().parse_with_language(input, language)
93    }
94
95    /// Parse a passage and retain book-resolution metadata.
96    pub fn parse_detailed(input: &str) -> Result<Parsed<Self>, ParseError> {
97        PassageParser::new().parse_detailed(input)
98    }
99
100    /// Parse with one explicit language and retain book-resolution metadata.
101    pub fn parse_detailed_with_language(
102        input: &str,
103        language: Language,
104    ) -> Result<Parsed<Self>, ParseError> {
105        PassageParser::new().parse_detailed_with_language(input, language)
106    }
107
108    /// Return `None` rather than an error for invalid input.
109    #[must_use]
110    pub fn try_parse(input: &str) -> Option<Self> {
111        PassageParser::new().try_parse(input)
112    }
113
114    /// Return `None` rather than an error when parsing with one language.
115    #[must_use]
116    pub fn try_parse_with_language(input: &str, language: Language) -> Option<Self> {
117        PassageParser::new().try_parse_with_language(input, language)
118    }
119
120    /// Borrow the whole-book value, if this is one.
121    #[must_use]
122    pub const fn as_book(&self) -> Option<&BookPassage> {
123        match self {
124            Self::Book(passage) => Some(passage),
125            _ => None,
126        }
127    }
128
129    /// Borrow the chapter value, if this is one.
130    #[must_use]
131    pub const fn as_chapter(&self) -> Option<&ChapterPassage> {
132        match self {
133            Self::Chapter(passage) => Some(passage),
134            _ => None,
135        }
136    }
137
138    /// Borrow the verse-selection value, if this is one.
139    #[must_use]
140    pub const fn as_verses(&self) -> Option<&VersePassage> {
141        match self {
142            Self::Verses(passage) => Some(passage),
143            _ => None,
144        }
145    }
146
147    /// Borrow the sequence value, if this is one.
148    #[must_use]
149    pub const fn as_sequence(&self) -> Option<&PassageSequence> {
150        match self {
151            Self::Sequence(passage) => Some(passage),
152            _ => None,
153        }
154    }
155}
156
157impl fmt::Display for Passage {
158    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::Book(passage) => passage.fmt(formatter),
161            Self::Chapter(passage) => passage.fmt(formatter),
162            Self::Verses(passage) => passage.fmt(formatter),
163            Self::Sequence(passage) => passage.fmt(formatter),
164        }
165    }
166}
167
168impl FromStr for Passage {
169    type Err = ParseError;
170
171    fn from_str(input: &str) -> Result<Self, Self::Err> {
172        Self::parse(input)
173    }
174}
175
176/// A passage covering an entire Bible book.
177#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
178pub struct BookPassage {
179    book: Book,
180}
181
182impl BookPassage {
183    /// Construct a whole-book passage.
184    #[must_use]
185    pub const fn new(book: Book) -> Self {
186        Self { book }
187    }
188
189    /// Return the represented book.
190    #[must_use]
191    pub const fn book(self) -> Book {
192        self.book
193    }
194}
195
196impl fmt::Display for BookPassage {
197    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198        formatter.write_str(self.book.full_name())
199    }
200}
201
202impl From<Book> for BookPassage {
203    fn from(book: Book) -> Self {
204        Self::new(book)
205    }
206}
207
208impl From<BookPassage> for Passage {
209    fn from(passage: BookPassage) -> Self {
210        Self::Book(passage)
211    }
212}
213
214/// A passage covering one chapter or an inclusive range of chapters.
215#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
216pub struct ChapterPassage {
217    book: Book,
218    start_chapter: u16,
219    end_chapter: Option<u16>,
220}
221
222impl ChapterPassage {
223    /// Construct a validated chapter or chapter-range passage.
224    pub const fn new(
225        book: Book,
226        start_chapter: u16,
227        end_chapter: Option<u16>,
228    ) -> Result<Self, PassageBuildError> {
229        if start_chapter == 0 || start_chapter > MAX_CHAPTER_NUMBER {
230            return Err(PassageBuildError::InvalidChapter {
231                value: start_chapter,
232            });
233        }
234        if let Some(end) = end_chapter {
235            if end == 0 || end > MAX_CHAPTER_NUMBER {
236                return Err(PassageBuildError::InvalidChapter { value: end });
237            }
238            if end <= start_chapter {
239                return Err(PassageBuildError::ChapterRangeNotAscending {
240                    start: start_chapter,
241                    end,
242                });
243            }
244        }
245        Ok(Self {
246            book,
247            start_chapter,
248            end_chapter,
249        })
250    }
251
252    /// Construct a validated single-chapter passage.
253    pub const fn single(book: Book, chapter: u16) -> Result<Self, PassageBuildError> {
254        Self::new(book, chapter, None)
255    }
256
257    /// Construct a validated inclusive chapter range.
258    pub const fn range(
259        book: Book,
260        start_chapter: u16,
261        end_chapter: u16,
262    ) -> Result<Self, PassageBuildError> {
263        Self::new(book, start_chapter, Some(end_chapter))
264    }
265
266    /// Return the represented book.
267    #[must_use]
268    pub const fn book(self) -> Book {
269        self.book
270    }
271
272    /// Return the first represented chapter.
273    #[must_use]
274    pub const fn start_chapter(self) -> u16 {
275        self.start_chapter
276    }
277
278    /// Return the inclusive last chapter for a range, or `None` for one chapter.
279    #[must_use]
280    pub const fn end_chapter(self) -> Option<u16> {
281        self.end_chapter
282    }
283}
284
285impl fmt::Display for ChapterPassage {
286    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
287        write!(
288            formatter,
289            "{} {}",
290            self.book.full_name(),
291            self.start_chapter
292        )?;
293        if let Some(end) = self.end_chapter {
294            write!(formatter, "-{end}")?;
295        }
296        Ok(())
297    }
298}
299
300impl From<ChapterPassage> for Passage {
301    fn from(passage: ChapterPassage) -> Self {
302        Self::Chapter(passage)
303    }
304}
305
306/// One or more discrete verse references belonging to one passage expression.
307#[derive(Clone, Debug, Eq, Hash, PartialEq)]
308pub struct VersePassage {
309    selections: Box<[Reference]>,
310}
311
312impl VersePassage {
313    /// Construct a non-empty verse-selection passage.
314    pub fn new(selections: impl IntoIterator<Item = Reference>) -> Result<Self, PassageBuildError> {
315        let selections = selections.into_iter().collect::<Box<[_]>>();
316        if selections.is_empty() {
317            return Err(PassageBuildError::EmptyVersePassage);
318        }
319        Ok(Self { selections })
320    }
321
322    /// Borrow the selections in source order.
323    #[must_use]
324    pub fn selections(&self) -> &[Reference] {
325        &self.selections
326    }
327}
328
329impl AsRef<[Reference]> for VersePassage {
330    fn as_ref(&self) -> &[Reference] {
331        self.selections()
332    }
333}
334
335impl fmt::Display for VersePassage {
336    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337        let first = self
338            .selections
339            .first()
340            .expect("VersePassage guarantees at least one selection");
341        first.fmt(formatter)?;
342
343        let anchor = first.start();
344        for selection in &self.selections[1..] {
345            formatter.write_str(",")?;
346            format_compact_selection(*selection, anchor, formatter)?;
347        }
348        Ok(())
349    }
350}
351
352impl TryFrom<Vec<Reference>> for VersePassage {
353    type Error = PassageBuildError;
354
355    fn try_from(selections: Vec<Reference>) -> Result<Self, Self::Error> {
356        Self::new(selections)
357    }
358}
359
360impl From<VersePassage> for Passage {
361    fn from(passage: VersePassage) -> Self {
362        Self::Verses(passage)
363    }
364}
365
366/// A source-ordered sequence of passage expressions.
367#[derive(Clone, Debug, Eq, Hash, PartialEq)]
368pub struct PassageSequence {
369    passages: Box<[Passage]>,
370}
371
372impl PassageSequence {
373    /// Construct a non-empty passage sequence.
374    pub fn new(passages: impl IntoIterator<Item = Passage>) -> Result<Self, PassageBuildError> {
375        let passages = passages.into_iter().collect::<Box<[_]>>();
376        if passages.is_empty() {
377            return Err(PassageBuildError::EmptyPassageSequence);
378        }
379        Ok(Self { passages })
380    }
381
382    /// Borrow the component passages in source order.
383    #[must_use]
384    pub fn passages(&self) -> &[Passage] {
385        &self.passages
386    }
387}
388
389impl AsRef<[Passage]> for PassageSequence {
390    fn as_ref(&self) -> &[Passage] {
391        self.passages()
392    }
393}
394
395impl fmt::Display for PassageSequence {
396    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397        for (index, passage) in self.passages.iter().enumerate() {
398            if index != 0 {
399                formatter.write_str("; ")?;
400            }
401            passage.fmt(formatter)?;
402        }
403        Ok(())
404    }
405}
406
407impl TryFrom<Vec<Passage>> for PassageSequence {
408    type Error = PassageBuildError;
409
410    fn try_from(passages: Vec<Passage>) -> Result<Self, Self::Error> {
411        Self::new(passages)
412    }
413}
414
415impl From<PassageSequence> for Passage {
416    fn from(passage: PassageSequence) -> Self {
417        Self::Sequence(passage)
418    }
419}
420
421/// A reusable parser for book, chapter, verse-list, and passage sequences.
422#[derive(Clone, Debug)]
423pub struct PassageParser {
424    reference_parser: ReferenceParser,
425    single_chapter_books: Vec<Book>,
426}
427
428impl Default for PassageParser {
429    fn default() -> Self {
430        Self {
431            reference_parser: ReferenceParser::new(),
432            single_chapter_books: DEFAULT_SINGLE_CHAPTER_BOOKS.to_vec(),
433        }
434    }
435}
436
437impl PassageParser {
438    /// Construct a parser with bundled aliases and conventional shorthand.
439    #[must_use]
440    pub fn new() -> Self {
441        Self::default()
442    }
443
444    /// Construct a passage parser around an existing reference parser.
445    #[must_use]
446    pub fn from_reference_parser(reference_parser: ReferenceParser) -> Self {
447        Self {
448            reference_parser,
449            ..Self::default()
450        }
451    }
452
453    /// Replace the books for which a bare number denotes a chapter-one verse.
454    #[must_use]
455    pub fn with_single_chapter_books(mut self, books: impl IntoIterator<Item = Book>) -> Self {
456        self.single_chapter_books.clear();
457        for book in books {
458            if !self.single_chapter_books.contains(&book) {
459                self.single_chapter_books.push(book);
460            }
461        }
462        self
463    }
464
465    /// Borrow the parser used for book-name resolution and reference parsing.
466    #[must_use]
467    pub const fn reference_parser(&self) -> &ReferenceParser {
468        &self.reference_parser
469    }
470
471    /// Return the books configured for bare-number verse shorthand.
472    #[must_use]
473    pub fn single_chapter_books(&self) -> &[Book] {
474        &self.single_chapter_books
475    }
476
477    /// Whether a book uses bare-number verse shorthand in this parser.
478    #[must_use]
479    pub fn is_single_chapter_book(&self, book: Book) -> bool {
480        self.single_chapter_books.contains(&book)
481    }
482
483    /// Parse a passage using automatic language detection.
484    pub fn parse(&self, input: &str) -> Result<Passage, ParseError> {
485        self.parse_detailed(input).map(Parsed::into_value)
486    }
487
488    /// Parse a passage using one explicit book-name language.
489    pub fn parse_with_language(
490        &self,
491        input: &str,
492        language: Language,
493    ) -> Result<Passage, ParseError> {
494        self.parse_detailed_with_language(input, language)
495            .map(Parsed::into_value)
496    }
497
498    /// Parse a passage and retain normalization and book-resolution metadata.
499    pub fn parse_detailed(&self, input: &str) -> Result<Parsed<Passage>, ParseError> {
500        self.parse_detailed_inner(input, Language::Auto)
501    }
502
503    /// Parse with an explicit language and retain resolution metadata.
504    pub fn parse_detailed_with_language(
505        &self,
506        input: &str,
507        language: Language,
508    ) -> Result<Parsed<Passage>, ParseError> {
509        self.parse_detailed_inner(input, language)
510    }
511
512    /// Return `None` rather than an error for invalid input.
513    #[must_use]
514    pub fn try_parse(&self, input: &str) -> Option<Passage> {
515        self.parse(input).ok()
516    }
517
518    /// Return `None` rather than an error when parsing with one language.
519    #[must_use]
520    pub fn try_parse_with_language(&self, input: &str, language: Language) -> Option<Passage> {
521        self.parse_with_language(input, language).ok()
522    }
523
524    fn parse_detailed_inner(
525        &self,
526        input: &str,
527        language: Language,
528    ) -> Result<Parsed<Passage>, ParseError> {
529        let normalized = normalize_for_parsing(input);
530        if normalized.is_empty() {
531            return Err(ParseError::new(
532                ParseErrorKind::EmptyReference,
533                "passage must not be empty",
534            ));
535        }
536
537        let source_segments = normalized.split(';').collect::<Vec<_>>();
538        if source_segments
539            .iter()
540            .any(|segment| segment.trim().is_empty())
541        {
542            return Err(ParseError::new(
543                ParseErrorKind::PatternMismatch,
544                "passage sequence contains an empty expression",
545            ));
546        }
547
548        let mut passages = Vec::with_capacity(source_segments.len());
549        let mut book_matches = Vec::new();
550        for segment in source_segments {
551            let parsed = self.parse_segment(segment.trim(), language)?;
552            passages.push(parsed.value);
553            book_matches.extend(parsed.book_matches);
554        }
555
556        let passage = if passages.len() == 1 {
557            passages
558                .pop()
559                .expect("one parsed source segment produces one passage")
560        } else {
561            Passage::Sequence(
562                PassageSequence::new(passages)
563                    .expect("multiple parsed segments form a non-empty sequence"),
564            )
565        };
566
567        Ok(Parsed::new(
568            passage,
569            ParseMetadata::from_parts(normalized, book_matches),
570        ))
571    }
572
573    fn parse_segment(&self, input: &str, language: Language) -> Result<ParsedSegment, ParseError> {
574        match self.parse_book(input, language) {
575            Ok((book, book_matches)) => {
576                return Ok(ParsedSegment::new(
577                    Passage::Book(BookPassage::new(book)),
578                    book_matches,
579                ));
580            }
581            Err(error) if error.kind() == ParseErrorKind::UnknownBook => {}
582            Err(error) => return Err(error),
583        }
584
585        let split = self.split_book_and_body(input, language)?;
586        let book = split.book;
587        let body = split.body;
588        let is_single_chapter = self.is_single_chapter_book(book);
589
590        if is_ascii_number(body) {
591            let component = if is_single_chapter {
592                "verse"
593            } else {
594                "chapter"
595            };
596            let maximum = if is_single_chapter {
597                MAX_VERSE_NUMBER
598            } else {
599                MAX_CHAPTER_NUMBER
600            };
601            let number = parse_number(body, component, maximum)?;
602            let value = if is_single_chapter {
603                let verse = VerseRef::new(book, 1, number)
604                    .expect("validated chapter and verse coordinates are valid");
605                Passage::Verses(
606                    VersePassage::new([Reference::Verse(verse)])
607                        .expect("a singleton verse passage is non-empty"),
608                )
609            } else {
610                Passage::Chapter(
611                    ChapterPassage::single(book, number)
612                        .expect("the parsed chapter coordinate is valid"),
613                )
614            };
615            return Ok(ParsedSegment::new(value, split.book_matches));
616        }
617
618        if let Some((start_token, end_token)) = exact_numeric_range(body) {
619            let component = if is_single_chapter {
620                "verse"
621            } else {
622                "chapter"
623            };
624            let maximum = if is_single_chapter {
625                MAX_VERSE_NUMBER
626            } else {
627                MAX_CHAPTER_NUMBER
628            };
629            let start = parse_number(start_token, &format!("start {component}"), maximum)?;
630            let end = parse_number(end_token, &format!("end {component}"), maximum)?;
631            if end <= start {
632                return Err(ParseError::new(
633                    ParseErrorKind::SameBookRangeNotAscending,
634                    format!("end {component} must come after start {component}"),
635                ));
636            }
637
638            let value = if is_single_chapter {
639                let start = VerseRef::new(book, 1, start)
640                    .expect("validated chapter and verse coordinates are valid");
641                let end = VerseRef::new(book, 1, end)
642                    .expect("validated chapter and verse coordinates are valid");
643                let range = VerseRange::new(start, end)
644                    .expect("the parsed range was checked to be ascending");
645                Passage::Verses(
646                    VersePassage::new([Reference::Range(range)])
647                        .expect("a singleton verse passage is non-empty"),
648                )
649            } else {
650                Passage::Chapter(
651                    ChapterPassage::range(book, start, end)
652                        .expect("the parsed chapter range is valid and ascending"),
653                )
654            };
655            return Ok(ParsedSegment::new(value, split.book_matches));
656        }
657
658        if body.contains(',') {
659            return self.parse_verse_list(split);
660        }
661
662        let reference_input = format!("{} {body}", split.book_token);
663        let parsed = self.parse_reference(&reference_input, language)?;
664        let (reference, metadata) = parsed.into_parts();
665        Ok(ParsedSegment::new(
666            Passage::Verses(
667                VersePassage::new([reference]).expect("a singleton verse passage is non-empty"),
668            ),
669            metadata.book_matches().to_vec(),
670        ))
671    }
672
673    fn parse_verse_list(&self, split: BookAndBody<'_>) -> Result<ParsedSegment, ParseError> {
674        let (chapter_token, selection_text) =
675            split_coordinate_once(split.body).ok_or_else(|| {
676                ParseError::new(
677                    ParseErrorKind::PatternMismatch,
678                    format!("verse list {:?} does not match expected format", split.body),
679                )
680            })?;
681        let chapter = parse_number(chapter_token, "chapter", MAX_CHAPTER_NUMBER)?;
682        if selection_text.is_empty() {
683            return Err(ParseError::new(
684                ParseErrorKind::PatternMismatch,
685                "verse list contains an empty selection",
686            ));
687        }
688
689        let tokens = selection_text.split(',').collect::<Vec<_>>();
690        if tokens.iter().any(|token| token.trim().is_empty()) {
691            return Err(ParseError::new(
692                ParseErrorKind::PatternMismatch,
693                "verse list contains an empty selection",
694            ));
695        }
696
697        let selections = tokens
698            .into_iter()
699            .map(|token| parse_verse_selection(token.trim(), split.book, chapter))
700            .collect::<Result<Vec<_>, _>>()?;
701        Ok(ParsedSegment::new(
702            Passage::Verses(
703                VersePassage::new(selections)
704                    .expect("a syntactically valid list contains at least one selection"),
705            ),
706            split.book_matches,
707        ))
708    }
709
710    fn split_book_and_body<'a>(
711        &self,
712        input: &'a str,
713        language: Language,
714    ) -> Result<BookAndBody<'a>, ParseError> {
715        let digit_starts = input
716            .char_indices()
717            .filter_map(|(index, character)| {
718                (index > 0 && character.is_ascii_digit()).then_some(index)
719            })
720            .collect::<Vec<_>>();
721        let mut last_unknown_book = None;
722
723        for position in digit_starts.into_iter().rev() {
724            let book_token = input[..position].trim();
725            let body = input[position..].trim();
726            if book_token.is_empty()
727                || !body.starts_with(|character: char| character.is_ascii_digit())
728            {
729                continue;
730            }
731            match self.parse_book(book_token, language) {
732                Ok((book, book_matches)) => {
733                    return Ok(BookAndBody {
734                        book_token,
735                        body,
736                        book,
737                        book_matches,
738                    });
739                }
740                Err(error) if error.kind() == ParseErrorKind::UnknownBook => {
741                    last_unknown_book = Some(error);
742                }
743                Err(error) => return Err(error),
744            }
745        }
746
747        Err(last_unknown_book.unwrap_or_else(|| {
748            ParseError::new(
749                ParseErrorKind::PatternMismatch,
750                format!("passage {input:?} does not match expected format"),
751            )
752        }))
753    }
754
755    fn parse_book(
756        &self,
757        input: &str,
758        language: Language,
759    ) -> Result<(Book, Vec<BookMatch>), ParseError> {
760        let parsed = self
761            .reference_parser
762            .parse_book_detailed_with_language(input, language)?;
763        let (book, metadata) = parsed.into_parts();
764        Ok((book, metadata.book_matches().to_vec()))
765    }
766
767    fn parse_reference(
768        &self,
769        input: &str,
770        language: Language,
771    ) -> Result<Parsed<Reference>, ParseError> {
772        self.reference_parser
773            .parse_detailed_with_language(input, language)
774    }
775}
776
777struct ParsedSegment {
778    value: Passage,
779    book_matches: Vec<BookMatch>,
780}
781
782impl ParsedSegment {
783    fn new(value: Passage, book_matches: Vec<BookMatch>) -> Self {
784        Self {
785            value,
786            book_matches,
787        }
788    }
789}
790
791struct BookAndBody<'a> {
792    book_token: &'a str,
793    body: &'a str,
794    book: Book,
795    book_matches: Vec<BookMatch>,
796}
797
798fn is_ascii_number(input: &str) -> bool {
799    !input.is_empty() && input.bytes().all(|byte| byte.is_ascii_digit())
800}
801
802fn exact_numeric_range(input: &str) -> Option<(&str, &str)> {
803    let (start, end) = input.split_once('-')?;
804    let start = start.trim();
805    let end = end.trim();
806    (!end.contains('-') && is_ascii_number(start) && is_ascii_number(end)).then_some((start, end))
807}
808
809fn split_coordinate_once(input: &str) -> Option<(&str, &str)> {
810    let separator = input
811        .char_indices()
812        .find(|(_, character)| matches!(character, ':' | '.'))?;
813    let left = input[..separator.0].trim();
814    let right = input[separator.0 + separator.1.len_utf8()..].trim();
815    (is_ascii_number(left) && !right.is_empty()).then_some((left, right))
816}
817
818fn parse_verse_selection(
819    input: &str,
820    book: Book,
821    default_chapter: u16,
822) -> Result<Reference, ParseError> {
823    let mut parts = input.split('-');
824    let start_text = parts.next().expect("split always returns one part").trim();
825    let end_text = parts.next().map(str::trim);
826    if parts.next().is_some() || start_text.is_empty() || end_text == Some("") {
827        return Err(ParseError::new(
828            ParseErrorKind::PatternMismatch,
829            format!("verse selection {input:?} does not match expected format"),
830        ));
831    }
832
833    let (start_chapter, start_verse) =
834        parse_selection_endpoint(start_text, default_chapter, "start")?;
835    let start = VerseRef::new(book, start_chapter, start_verse)
836        .expect("parsed chapter and verse coordinates are valid");
837
838    let Some(end_text) = end_text else {
839        return Ok(Reference::Verse(start));
840    };
841    let (end_chapter, end_verse) = parse_selection_endpoint(end_text, start_chapter, "end")?;
842    let end = VerseRef::new(book, end_chapter, end_verse)
843        .expect("parsed chapter and verse coordinates are valid");
844    let range = VerseRange::new(start, end).map_err(|_| {
845        ParseError::new(
846            ParseErrorKind::SameBookRangeNotAscending,
847            "end reference must come after start reference",
848        )
849    })?;
850    Ok(Reference::Range(range))
851}
852
853fn parse_selection_endpoint(
854    input: &str,
855    default_chapter: u16,
856    position: &str,
857) -> Result<(u16, u16), ParseError> {
858    if let Some((chapter, verse)) = split_coordinate_once(input) {
859        if verse.contains([':', '.']) || !is_ascii_number(verse) {
860            return Err(ParseError::new(
861                ParseErrorKind::PatternMismatch,
862                format!("verse selection endpoint {input:?} does not match expected format"),
863            ));
864        }
865        return Ok((
866            parse_number(chapter, &format!("{position} chapter"), MAX_CHAPTER_NUMBER)?,
867            parse_number(verse, &format!("{position} verse"), MAX_VERSE_NUMBER)?,
868        ));
869    }
870
871    if !is_ascii_number(input) {
872        return Err(ParseError::new(
873            ParseErrorKind::PatternMismatch,
874            format!("verse selection endpoint {input:?} does not match expected format"),
875        ));
876    }
877    Ok((
878        default_chapter,
879        parse_number(input, &format!("{position} verse"), MAX_VERSE_NUMBER)?,
880    ))
881}
882
883fn parse_number(token: &str, component: &str, maximum: u16) -> Result<u16, ParseError> {
884    let value = token.parse::<u32>().map_err(|_| {
885        ParseError::new(
886            ParseErrorKind::InvalidNumericToken,
887            format!("{component} token {token:?} is not an integer"),
888        )
889    })?;
890    if value == 0 {
891        return Err(ParseError::new(
892            ParseErrorKind::NonPositiveNumericToken,
893            format!("{component} must be greater than zero"),
894        ));
895    }
896    if value > u32::from(maximum) {
897        return Err(ParseError::new(
898            ParseErrorKind::NumericTokenOutOfRange,
899            format!("{component} {value} exceeds the sanity limit {maximum}"),
900        ));
901    }
902    Ok(value as u16)
903}
904
905fn format_compact_selection(
906    reference: Reference,
907    anchor: VerseRef,
908    formatter: &mut fmt::Formatter<'_>,
909) -> fmt::Result {
910    let start = reference.start();
911    if start.book() != anchor.book() {
912        return fmt::Display::fmt(&reference, formatter);
913    }
914
915    if start.chapter() == anchor.chapter() {
916        write!(formatter, "{}", start.verse())?;
917    } else {
918        write!(formatter, "{}:{}", start.chapter(), start.verse())?;
919    }
920
921    let Reference::Range(range) = reference else {
922        return Ok(());
923    };
924    let end = range.end();
925    if end.book() != start.book() {
926        write!(formatter, "-{end}")
927    } else if end.chapter() == start.chapter() {
928        write!(formatter, "-{}", end.verse())
929    } else {
930        write!(formatter, "-{}:{}", end.chapter(), end.verse())
931    }
932}
933
934#[cfg(test)]
935#[path = "../tests/unit/passage.rs"]
936mod tests;