1use 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
17pub 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
28#[non_exhaustive]
29pub enum PassageBuildError {
30 InvalidChapter {
32 value: u16,
34 },
35 ChapterRangeNotAscending {
37 start: u16,
39 end: u16,
41 },
42 EmptyVersePassage,
44 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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
73pub enum Passage {
74 Book(BookPassage),
76 Chapter(ChapterPassage),
78 Verses(VersePassage),
80 Sequence(PassageSequence),
82}
83
84impl Passage {
85 pub fn parse(input: &str) -> Result<Self, ParseError> {
87 PassageParser::new().parse(input)
88 }
89
90 pub fn parse_with_language(input: &str, language: Language) -> Result<Self, ParseError> {
92 PassageParser::new().parse_with_language(input, language)
93 }
94
95 pub fn parse_detailed(input: &str) -> Result<Parsed<Self>, ParseError> {
97 PassageParser::new().parse_detailed(input)
98 }
99
100 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 #[must_use]
110 pub fn try_parse(input: &str) -> Option<Self> {
111 PassageParser::new().try_parse(input)
112 }
113
114 #[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 #[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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
178pub struct BookPassage {
179 book: Book,
180}
181
182impl BookPassage {
183 #[must_use]
185 pub const fn new(book: Book) -> Self {
186 Self { book }
187 }
188
189 #[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#[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 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 pub const fn single(book: Book, chapter: u16) -> Result<Self, PassageBuildError> {
254 Self::new(book, chapter, None)
255 }
256
257 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 #[must_use]
268 pub const fn book(self) -> Book {
269 self.book
270 }
271
272 #[must_use]
274 pub const fn start_chapter(self) -> u16 {
275 self.start_chapter
276 }
277
278 #[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
308pub struct VersePassage {
309 selections: Box<[Reference]>,
310}
311
312impl VersePassage {
313 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 #[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
368pub struct PassageSequence {
369 passages: Box<[Passage]>,
370}
371
372impl PassageSequence {
373 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 #[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#[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 #[must_use]
440 pub fn new() -> Self {
441 Self::default()
442 }
443
444 #[must_use]
446 pub fn from_reference_parser(reference_parser: ReferenceParser) -> Self {
447 Self {
448 reference_parser,
449 ..Self::default()
450 }
451 }
452
453 #[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 #[must_use]
467 pub const fn reference_parser(&self) -> &ReferenceParser {
468 &self.reference_parser
469 }
470
471 #[must_use]
473 pub fn single_chapter_books(&self) -> &[Book] {
474 &self.single_chapter_books
475 }
476
477 #[must_use]
479 pub fn is_single_chapter_book(&self, book: Book) -> bool {
480 self.single_chapter_books.contains(&book)
481 }
482
483 pub fn parse(&self, input: &str) -> Result<Passage, ParseError> {
485 self.parse_detailed(input).map(Parsed::into_value)
486 }
487
488 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 pub fn parse_detailed(&self, input: &str) -> Result<Parsed<Passage>, ParseError> {
500 self.parse_detailed_inner(input, Language::Auto)
501 }
502
503 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 #[must_use]
514 pub fn try_parse(&self, input: &str) -> Option<Passage> {
515 self.parse(input).ok()
516 }
517
518 #[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;