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}