bible_lib/
lib.rs

1/*
2              Bible Lib
3                .---.
4           '-.  |   |  .-'
5             ___|   |___
6        -=  [           ]  =-
7            `---.   .---'
8         __||__ |   | __||__
9         '-..-' |   | '-..-'
10           ||   |   |   ||
11           ||_.-|   |-,_||
12         .-"`   `"`'`   `"-.
13       .'                   '. Art by Joan Stark
14*/
15
16use std::{collections::HashMap, fmt::Display};
17
18use crate::error::BibleLibError;
19
20pub mod error;
21
22#[cfg(feature = "akjv")]
23const AKJV: &str = include_str!("bible_translations/akjv.txt");
24#[cfg(feature = "asv")]
25const ASV: &str = include_str!("bible_translations/asv.txt");
26#[cfg(feature = "erv")]
27const ERV: &str = include_str!("bible_translations/erv.txt");
28#[cfg(feature = "kjv")]
29const KJV: &str = include_str!("bible_translations/kjv.txt");
30
31/// Different Bible Translations
32/// provided by https://openbible.com/
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Translation {
35    /// American King James Version
36    #[cfg(feature = "akjv")]
37    AmericanKingJames,
38    /// American Standard Version
39    #[cfg(feature = "asv")]
40    AmericanStandard,
41    /// English Revised Version
42    #[cfg(feature = "erv")]
43    EnglishRevised,
44    /// King James Version
45    #[cfg(feature = "kjv")]
46    KingJames,
47    /// For custom translations,
48    /// each line must be a verse formatted as: `Book Chapter:Verse Content`
49    /// See bible_translations/ for examples
50    ///
51    /// `name` is strictly for display purposes
52    ///
53    /// note: other translations are included in the binary at compile time,
54    /// but custom translations are read from the filesystem at runtime
55    Custom { name: String, path: String },
56}
57
58impl Translation {
59    #[doc(hidden)]
60    fn get_text(&self) -> Result<String, BibleLibError> {
61        match self {
62            Self::AmericanKingJames => Ok(AKJV.to_string()),
63            Self::AmericanStandard => Ok(ASV.to_string()),
64            Self::EnglishRevised => Ok(ERV.to_string()),
65            Self::KingJames => Ok(KJV.to_string()),
66            Self::Custom { path, .. } => {
67                // ensure the file exists
68                if !std::path::Path::new(path).exists() {
69                    return Err(BibleLibError::InvalidCustomTranslationFile);
70                }
71
72                // read the file and return the content
73                let result = std::fs::read_to_string(path);
74                match result {
75                    Ok(content) => Ok(content),
76                    Err(e) => Err(BibleLibError::IOError(e)),
77                }
78            }
79        }
80    }
81}
82
83#[cfg(any(feature = "akjv", feature = "asv", feature = "erv", feature = "kjv"))]
84impl Default for Translation {
85    #[cfg(feature = "akjv")]
86    fn default() -> Self {
87        Self::AmericanKingJames
88    }
89    #[cfg(all(not(feature = "akjv"), feature = "asv"))]
90    fn default() -> Self {
91        Self::AmericanStandard
92    }
93    #[cfg(all(not(feature = "akjv"), not(feature = "asv"), feature = "erv"))]
94    fn default() -> Self {
95        Self::EnglishRevised
96    }
97    #[cfg(all(
98        not(feature = "akjv"),
99        not(feature = "asv"),
100        not(feature = "erv"),
101        feature = "kjv"
102    ))]
103    fn default() -> Self {
104        Self::KingJames
105    }
106}
107
108impl Display for Translation {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            #[cfg(feature = "akjv")]
112            Self::AmericanKingJames => write!(f, "American King James Version"),
113            #[cfg(feature = "asv")]
114            Self::AmericanStandard => write!(f, "American Standard Version"),
115            #[cfg(feature = "erv")]
116            Self::EnglishRevised => write!(f, "English Revised Version"),
117            #[cfg(feature = "kjv")]
118            Self::KingJames => write!(f, "King James Version"),
119            Self::Custom { name, .. } => write!(f, "Custom Translation: {}", name),
120        }
121    }
122}
123
124/// Struct representing a Bible verse lookup
125/// `book` is not case-sensitive
126/// `thru_verse` is optional and used for verse ranges like `John 3:16-18`
127/// # Example
128/// ```
129/// use bible_lib::{Bible, BibleLookup, Translation};
130///
131/// // get the bible translation
132/// let bible = Bible::new(Translation::KingJames).unwrap();
133/// // create a lookup for John 3:16
134/// let lookup = BibleLookup::new("John", 3, 16);
135/// // get the verse text
136/// let verse = bible.get_verse(lookup, false).unwrap();
137///
138/// // print the verse text
139/// println!("John 3:16: {}", verse);
140/// ```
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct BibleLookup {
143    pub book: String,
144    pub chapter: u32,
145    pub verse: u32,
146    pub thru_verse: Option<u32>,
147}
148
149impl BibleLookup {
150    /// Create a new BibleLookup instance (single verse)
151    /// `book` is not case-sensitive
152    /// # Example
153    /// ```
154    /// use bible_lib::BibleLookup;
155    ///
156    /// // create a lookup for John 3:16
157    /// let lookup = BibleLookup::new("John", 3, 16);
158    /// ```
159    pub fn new<S: Into<String>>(book: S, chapter: u32, verse: u32) -> Self {
160        let book = book.into();
161        let book = book.to_lowercase();
162        Self {
163            book,
164            chapter,
165            verse,
166            thru_verse: None,
167        }
168    }
169
170    /// Create a new BibleLookup instance (verse range)
171    /// # Example
172    /// ```
173    /// use bible_lib::BibleLookup;
174    ///
175    /// // create a lookup for Luke 23:39-43
176    /// let lookup = BibleLookup::new_range("Luke", 23, 39, 43);
177    /// ```
178    pub fn new_range<S: Into<String>>(book: S, chapter: u32, verse: u32, thru_verse: u32) -> Self {
179        let book = book.into();
180        let book = book.to_lowercase();
181        Self {
182            book,
183            chapter,
184            verse,
185            thru_verse: Some(thru_verse),
186        }
187    }
188
189    /// Detect Bible verses in a string
190    /// Requires the `detection` feature to be enabled
191    /// Can return multiple verses if more than one is found
192    /// # Example
193    /// ```
194    /// use bible_lib::{Bible, Translation, BibleLookup};
195    ///
196    /// // get the bible translation
197    /// let bible = Bible::new(Translation::default()).unwrap();
198    ///
199    /// // create the string to look for verses in
200    /// let text = "Show me John 3:16";
201    /// // detect verses in the string
202    /// let verses = BibleLookup::detect_from_string(text);
203    ///
204    /// // iterate through the found verses and print them
205    /// for verse in verses {
206    ///     // get the verse text
207    ///     let verse_text = bible.get_verse(verse.clone()).unwrap();
208    ///     // print the verse text
209    ///     println!("Found verse: {} - {}", verse, verse_text);
210    /// }
211    /// ```
212    #[cfg(feature = "detection")]
213    pub fn detect_from_string<S: Into<String>>(lookup: S) -> Vec<Self> {
214        let mut verses = Vec::new();
215
216        let lookup = lookup.into();
217        let text = lookup.to_lowercase();
218
219        //let regex = regex::Regex::new(r"\b(?:genesis|exodus|leviticus|numbers|deuteronomy|joshua|judges|ruth|1\s?samuel|2\s?samuel|1\s?kings|2\s?kings|1\s?chronicles|2\s?chronicles|ezra|nehemiah|esther|job|psalms|proverbs|ecclesiastes|song\sof\ssolomon|isaiah|jeremiah|lamentations|ezekiel|daniel|hosea|joel|amos|obadiah|jonah|micah|nahum|habakkuk|zephaniah|haggai|zechariah|malachi|matthew|mark|luke|john|acts|romans|1\s?corinthians|2\s?corinthians|galatians|ephesians|philippians|colossians|1\s?thessalonians|2\s?thessalonians|1\s?timothy|2\s?timothy|titus|philemon|hebrews|james|1\s?peter|2\s?peter|1\s?john|2\s?john|3\s?john|jude|revelation)\s+\d+:\d+\b").unwrap();
220        let regex = regex::Regex::new(r"\b(?:genesis|exodus|leviticus|numbers|deuteronomy|joshua|judges|ruth|1\s?samuel|2\s?samuel|1\s?kings|2\s?kings|1\s?chronicles|2\s?chronicles|ezra|nehemiah|esther|job|psalms|proverbs|ecclesiastes|song\sof\ssolomon|isaiah|jeremiah|lamentations|ezekiel|daniel|hosea|joel|amos|obadiah|jonah|micah|nahum|habakkuk|zephaniah|haggai|zechariah|malachi|matthew|mark|luke|john|acts|romans|1\s?corinthians|2\s?corinthians|galatians|ephesians|philippians|colossians|1\s?thessalonians|2\s?thessalonians|1\s?timothy|2\s?timothy|titus|philemon|hebrews|james|1\s?peter|2\s?peter|1\s?john|2\s?john|3\s?john|jude|revelation)\s+\d+:\d+(?:-\d+)?\b").unwrap();
221
222        for instance in regex.find_iter(&text) {
223            let instance = instance.as_str();
224            // to handle cases like `1 samuel` and `Song of Solomon`, split by ':' first and then split by whitespace
225            let mut parts = instance.split(':');
226            // split the first part by whitespace
227            let book_chapter = parts.next().unwrap().split_whitespace();
228            let count = book_chapter.clone().count();
229            let chapter = book_chapter.clone().last().unwrap().parse::<u32>().unwrap();
230            let book = book_chapter
231                .take(count - 1)
232                .collect::<Vec<&str>>()
233                .join(" ")
234                .to_lowercase();
235
236            // handle cases where the verse is a range (i.e. `1-3`)
237            let verse_part = parts.next().unwrap();
238            if verse_part.contains('-') {
239                let verse_split = verse_part.split('-');
240                let verse = verse_split.clone().next().unwrap().parse::<u32>().unwrap();
241                let thru_verse = verse_split.clone().last().unwrap().parse::<u32>().unwrap();
242                verses.push(BibleLookup {
243                    book,
244                    chapter,
245                    verse,
246                    thru_verse: Some(thru_verse),
247                });
248            } else {
249                let verse = verse_part.parse::<u32>().unwrap();
250                verses.push(BibleLookup {
251                    book,
252                    chapter,
253                    verse,
254                    thru_verse: None,
255                });
256            }
257        }
258
259        verses
260    }
261
262    /// Capitalize the first letter of each word in the book name
263    /// Handles cases like `1 samuel` and `song of solomon`
264    /// This is used because book names are stored in lowercase for easier lookup
265    /// # Example
266    /// ```
267    /// use bible_lib::BibleLookup;
268    ///
269    /// // capitalize book names
270    /// let book1 = BibleLookup::capitalize_book(&"john".to_string());
271    /// let book2 = BibleLookup::capitalize_book(&"1 samuel".to_string());
272    ///
273    /// // print the capitalized book names
274    /// println!("Capitalized Book 1: {}", book1); // John
275    /// println!("Capitalized Book 2: {}", book2); // 1 Samuel
276    ///
277    /// ```
278    pub fn capitalize_book(name: &String) -> String {
279        // capitalize the first letter of each word in the book name
280        // Split the input string by whitespace into words
281        name.split_whitespace()
282            // For each word, apply the following transformation
283            .map(|word| {
284                // Convert the word into characters
285                let mut chars = word.chars();
286                // If there's a first character, convert it to uppercase and concatenate it with the rest of the characters
287                if let Some(first_char) = chars.next() {
288                    if first_char.is_numeric() {
289                        // If the first character is numeric, leave it unchanged
290                        first_char.to_string() + &chars.collect::<String>()
291                    } else {
292                        // If the first character is not numeric, capitalize it
293                        first_char.to_uppercase().chain(chars).collect::<String>()
294                    }
295                } else {
296                    // If the word is empty, return an empty string
297                    String::new()
298                }
299            })
300            // Collect the transformed words back into a single string, separated by whitespace
301            .collect::<Vec<String>>()
302            .join(" ")
303    }
304}
305
306impl Display for BibleLookup {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        if let Some(thru_verse) = self.thru_verse {
309            write!(
310                f,
311                "{} {}:{}-{}",
312                Self::capitalize_book(&self.book),
313                self.chapter,
314                self.verse,
315                thru_verse
316            )
317        } else {
318            write!(
319                f,
320                "{} {}:{}",
321                Self::capitalize_book(&self.book),
322                self.chapter,
323                self.verse
324            )
325        }
326    }
327}
328
329/// Main Bible struct
330/// Stores the verses of the Bible for interfacing
331/// # Example
332/// ```
333/// use bible_lib::{Bible, Translation, BibleLookup};
334///
335/// // get the bible translation
336/// let bible = Bible::new(Translation::AmericanStandard).unwrap();
337///
338/// // create a lookup for John 3:16
339/// let lookup = BibleLookup::new("John", 3, 16);
340/// // get the verse text
341/// let verse = bible.get_verse(lookup, false).unwrap();
342///
343/// // print the verse text
344/// println!("John 3:16: {}", verse);
345/// ```
346#[derive(Debug, Clone)]
347pub struct Bible {
348    translation: Translation,
349    pub verses: HashMap<
350        String, /* Book */
351        HashMap<u32 /* Chapter */, HashMap<u32 /* Verse */, String /* Text */>>,
352    >,
353}
354
355impl Bible {
356    #[doc(hidden)]
357    fn parse_text(lines: &String) -> HashMap<String, HashMap<u32, HashMap<u32, String>>> {
358        let mut verses = HashMap::new();
359
360        for line in lines.lines() {
361            // to handle cases like `1 samuel` and `Song of Solomon`, split by ':' first and then split by whitespace
362            let mut parts = line.split(':');
363            // split the first part by whitespace
364            let book_chapter = parts.next().unwrap().split_whitespace();
365            let count = book_chapter.clone().count();
366            let chapter = book_chapter.clone().last().unwrap().parse::<u32>().unwrap();
367            let book = book_chapter
368                .take(count - 1)
369                .collect::<Vec<&str>>()
370                .join(" ")
371                .to_lowercase();
372
373            let verse_text = parts.next().unwrap().split_whitespace();
374            let verse = verse_text.clone().next().unwrap().parse::<u32>().unwrap();
375            let text = verse_text.clone().skip(1).collect::<Vec<&str>>().join(" ");
376
377            if !verses.contains_key(&book) {
378                verses.insert(book.to_string(), HashMap::new());
379            }
380            if !verses.get_mut(&book).unwrap().contains_key(&chapter) {
381                verses
382                    .get_mut(&book)
383                    .unwrap()
384                    .insert(chapter, HashMap::new());
385            }
386            verses
387                .get_mut(&book)
388                .unwrap()
389                .get_mut(&chapter)
390                .unwrap()
391                .insert(verse, text.to_string());
392        }
393
394        verses
395    }
396
397    /// Create a new Bible instance with the specified translation
398    pub fn new(translation: Translation) -> Result<Self, BibleLibError> {
399        let text = translation.get_text()?;
400        let verses = Self::parse_text(&text);
401        Ok(Self {
402            translation,
403            verses,
404        })
405    }
406
407    /// Get the current translation of the Bible instance
408    pub fn get_translation(&self) -> &Translation {
409        &self.translation
410    }
411
412    #[doc(hidden)]
413    fn replace_superscript(s: String) -> String {
414        s.chars()
415            .map(|c| match c {
416                '0' => '⁰',
417                '1' => '¹',
418                '2' => '²',
419                '3' => '³',
420                '4' => '⁴',
421                '5' => '⁵',
422                '6' => '⁶',
423                '7' => '⁷',
424                '8' => '⁸',
425                '9' => '⁹',
426                _ => c,
427            })
428            .collect()
429    }
430
431    /// Get the text of a verse or range of verses
432    /// `use_superscripts` adds superscript verse numbers for better readability
433    /// Returns an error if the verse or chapter is not found
434    /// # Example
435    /// ```
436    /// use bible_lib::{Bible, BibleLookup, Translation};
437    ///
438    /// // get the bible translation
439    /// let bible = Bible::new(Translation::AmericanStandard).unwrap();
440    /// // create a lookup for John 3:16
441    /// let lookup = BibleLookup::new("John", 3, 16);
442    /// // get the verse text
443    /// let verse = bible.get_verse(lookup, false).unwrap();
444    ///
445    /// // print the verse text
446    /// println!("John 3:16: {}", verse);
447    /// ```
448    pub fn get_verse(
449        &self,
450        lookup: BibleLookup,
451        use_superscripts: bool,
452    ) -> Result<String, BibleLibError> {
453        // multiple verse lookup
454        if let Some(thru_verse) = lookup.thru_verse {
455            let mut verse_text = String::new();
456
457            // iterate through the verses
458            for verse in lookup.verse..=thru_verse {
459                let Some(chapters) = self.verses.get(&lookup.book) else {
460                    return Err(BibleLibError::BookNotFound);
461                };
462                let Some(verses) = chapters.get(&lookup.chapter) else {
463                    return Err(BibleLibError::ChapterNotFound);
464                };
465                let Some(text) = verses.get(&verse) else {
466                    return Err(BibleLibError::VerseNotFound);
467                };
468
469                if use_superscripts {
470                    verse_text.push_str(&format!(
471                        "{}{} ",
472                        Self::replace_superscript(verse.to_string()),
473                        text
474                    ));
475                } else {
476                    verse_text.push_str(text);
477                }
478            }
479            return Ok(verse_text.trim().to_string());
480        }
481
482        // single verse lookup
483        let Some(chapters) = self.verses.get(&lookup.book) else {
484            return Err(BibleLibError::BookNotFound);
485        };
486        let Some(verses) = chapters.get(&lookup.chapter) else {
487            return Err(BibleLibError::ChapterNotFound);
488        };
489        let Some(text) = verses.get(&lookup.verse) else {
490            return Err(BibleLibError::VerseNotFound);
491        };
492
493        if use_superscripts {
494            Ok(format!(
495                "{}{}",
496                Self::replace_superscript(lookup.verse.to_string()),
497                text
498            ))
499        } else {
500            Ok(text.to_string())
501        }
502    }
503
504    /// Get the text of an entire chapter as a string
505    /// `use_superscripts` adds superscript verse numbers for better readability
506    /// Returns an error if the chapter is not found
507    /// # Example
508    /// ```
509    /// use bible_lib::{Bible, BibleLookup, Translation};
510    ///
511    /// // get the bible translation
512    /// let bible = Bible::new(Translation::EnglishRevised).unwrap();
513    /// // get the text of Isaiah chapter 53
514    /// let chapter_text = bible.get_chapter("Isaiah", 53, true).unwrap();
515    ///
516    /// // print the chapter text
517    /// println!("Isaiah 53: {}", chapter_text);
518    /// ```
519    pub fn get_chapter(
520        &self,
521        book: &str,
522        chapter: u32,
523        use_superscripts: bool,
524    ) -> Result<String, BibleLibError> {
525        let mut chapter_text = String::new();
526        // sort the verses by verse number
527        let Some(chapters) = self.verses.get(book) else {
528            return Err(BibleLibError::BookNotFound);
529        };
530        let Some(verses) = chapters.get(&chapter) else {
531            return Err(BibleLibError::ChapterNotFound);
532        };
533        let mut verses = verses.iter().collect::<Vec<(&u32, &String)>>();
534        verses.sort_by(|a, b| a.0.cmp(b.0));
535        for (verse, text) in verses {
536            let verse_designation = Self::replace_superscript(verse.to_string());
537            if use_superscripts {
538                chapter_text.push_str(&format!("{}{} ", verse_designation, text));
539            } else {
540                chapter_text.push_str(&format!("{} ", text));
541            }
542        }
543        Ok(chapter_text)
544    }
545
546    /// Get a list of all books in the Bible
547    /// # Example
548    /// ```
549    /// use bible_lib::{Bible, Translation};
550    ///
551    /// // get the bible translation
552    /// let bible = Bible::new(Translation::default()).unwrap();
553    ///
554    /// // get the list of books
555    /// let books = bible.get_books();
556    /// // print the list of books
557    /// println!("Books in the Bible: {:?}", books);
558    /// ```
559    pub fn get_books(&self) -> Vec<String> {
560        self.verses.keys().map(|s| s.to_string()).collect()
561    }
562
563    /// Get a list of all books in the Bible, sorted in canonical order
564    /// # Example
565    /// ```
566    /// use bible_lib::{Bible, Translation};
567    ///
568    /// // get the bible translation
569    /// let bible = Bible::new(Translation::default()).unwrap();
570    ///
571    /// // get the list of books
572    /// let books = bible.get_sorted_books();
573    /// // print the list of books
574    /// println!("Books in the Bible: {:?}", books);
575    /// ```
576    pub fn get_sorted_books(&self) -> Vec<String> {
577        let mut books = self.get_books();
578        let canonical_order = vec![
579            "genesis",
580            "exodus",
581            "leviticus",
582            "numbers",
583            "deuteronomy",
584            "joshua",
585            "judges",
586            "ruth",
587            "1 samuel",
588            "2 samuel",
589            "1 kings",
590            "2 kings",
591            "1 chronicles",
592            "2 chronicles",
593            "ezra",
594            "nehemiah",
595            "esther",
596            "job",
597            "psalms",
598            "psalm", // double entry for psalms because some translations use singular
599            "proverbs",
600            "ecclesiastes",
601            "song of solomon",
602            "isaiah",
603            "jeremiah",
604            "lamentations",
605            "ezekiel",
606            "daniel",
607            "hosea",
608            "joel",
609            "amos",
610            "obadiah",
611            "jonah",
612            "micah",
613            "nahum",
614            "habakkuk",
615            "zephaniah",
616            "haggai",
617            "zechariah",
618            "malachi",
619            "matthew",
620            "mark",
621            "luke",
622            "john",
623            "acts",
624            "romans",
625            "1 corinthians",
626            "2 corinthians",
627            "galatians",
628            "ephesians",
629            "philippians",
630            "colossians",
631            "1 thessalonians",
632            "2 thessalonians",
633            "1 timothy",
634            "2 timothy",
635            "titus",
636            "philemon",
637            "hebrews",
638            "james",
639            "1 peter",
640            "2 peter",
641            "1 john",
642            "2 john",
643            "3 john",
644            "jude",
645            "revelation",
646        ];
647        books.sort_by_key(|book| {
648            canonical_order
649                .iter()
650                .position(|&b| b == book.as_str())
651                .unwrap_or(usize::MAX)
652        });
653        books
654    }
655
656    /// Get a list of all chapters in a book
657    /// # Example
658    /// ```
659    /// use bible_lib::{Bible, Translation};
660    ///
661    /// // get the bible translation
662    /// let bible = Bible::new(Translation::default()).unwrap();
663    ///
664    /// // get the list of chapters in Revelation
665    /// let chapters = bible.get_chapters("Revelation").unwrap();
666    /// // print the list of chapters
667    /// println!("Chapters in Revelation: {:?}", chapters);
668    /// ```
669    pub fn get_chapters(&self, book: &str) -> Result<Vec<u32>, BibleLibError> {
670        if let Some(chapters) = self
671            .verses
672            .get(book)
673            .map(|chapters| chapters.keys().map(|c| *c).collect())
674        {
675            Ok(chapters)
676        } else {
677            Err(BibleLibError::BookNotFound)
678        }
679    }
680
681    /// Get a list of all verses in a chapter of a book
682    /// # Example
683    /// ```
684    /// use bible_lib::{Bible, Translation};
685    ///
686    /// // get the bible translation
687    /// let bible = Bible::new(Translation::default()).unwrap();
688    ///
689    /// // get the list of verses in John chapter 3
690    /// let verses = bible.get_verses("John", 3).unwrap();
691    /// // print the list of verses
692    /// println!("Verses in John 3: {:?}", verses);
693    /// ```
694    pub fn get_verses(&self, book: &str, chapter: u32) -> Result<Vec<u32>, BibleLibError> {
695        if let Some(verses) = self
696            .verses
697            .get(book)
698            .and_then(|chapters| chapters.get(&chapter))
699            .map(|verses| verses.keys().map(|v| *v).collect())
700        {
701            Ok(verses)
702        } else {
703            Err(BibleLibError::ChapterNotFound)
704        }
705    }
706
707    /// Get the maximum verse number in a chapter of a book
708    pub fn get_max_verse(&self, book: &str, chapter: u32) -> Result<u32, BibleLibError> {
709        if let Some(verses) = self
710            .verses
711            .get(book)
712            .and_then(|chapters| chapters.get(&chapter))
713        {
714            if let Some(max_verse) = verses.keys().max() {
715                Ok(*max_verse)
716            } else {
717                Err(BibleLibError::ChapterNotFound)
718            }
719        } else {
720            Err(BibleLibError::ChapterNotFound)
721        }
722    }
723
724    /// Get the maximum chapter number in a chapter of a book
725    pub fn get_max_chapter(&self, book: &str) -> Result<u32, BibleLibError> {
726        if let Some(chapters) = self.verses.get(book) {
727            if let Some(max_chapter) = chapters.keys().max() {
728                Ok(*max_chapter)
729            } else {
730                Err(BibleLibError::BookNotFound)
731            }
732        } else {
733            Err(BibleLibError::BookNotFound)
734        }
735    }
736
737    /// Get a random verse from the Bible
738    /// Requires the `random` feature to be enabled
739    /// # Example
740    /// ```
741    /// use bible_lib::{Bible, Translation};
742    ///
743    /// // get the bible translation
744    /// let bible = Bible::new(Translation::default()).unwrap();
745    ///
746    /// // get a random verse
747    /// let random_verse = bible.random_verse();
748    /// // get the verse text
749    /// let verse_text = bible.get_verse(random_verse.clone(), false).unwrap();
750    /// // print the random verse
751    /// println!("Random Verse: {} - {}", random_verse, verse_text);
752    /// ```
753    #[cfg(feature = "random")]
754    pub fn random_verse(&self) -> BibleLookup {
755        use rand::seq::IteratorRandom;
756        let mut rng = rand::rng();
757        let book = self.verses.keys().choose(&mut rng).unwrap().to_string();
758        let chapters = self.verses.get(&book).unwrap();
759        let chapter = chapters.keys().choose(&mut rng).unwrap().to_owned();
760        let verses = chapters.get(&chapter).unwrap();
761        let verse = verses.keys().choose(&mut rng).unwrap().to_owned();
762        BibleLookup {
763            book,
764            chapter,
765            verse,
766            thru_verse: None,
767        }
768    }
769}