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/// https://openbible.com/texts.htm
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum Translation {
36    /// American King James Version
37    #[cfg(feature = "akjv")]
38    AmericanKingJames,
39    /// American Standard Version
40    #[cfg(feature = "asv")]
41    AmericanStandard,
42    /// English Revised Version
43    #[cfg(feature = "erv")]
44    EnglishedRevised,
45    /// King James Version
46    #[cfg(feature = "kjv")]
47    KingJames,
48    /// For custom translations,
49    /// each line must be a verse formatted as: `Book Chapter:Verse Content`
50    /// See bible_translations/ for examples
51    /// 
52    /// `name` is strictly for display purposes
53    ///
54    /// note: other translations are included in the binary at compile time,
55    /// but custom translations are read from the filesystem at runtime
56    Custom { name: String, path: String }
57}
58
59impl Translation {
60    #[doc(hidden)]
61    fn get_text(&self) -> Result<String, BibleLibError> {
62        match self {
63
64            Self::AmericanKingJames => {
65                Ok(AKJV.to_string())
66            }
67            Self::AmericanStandard => {
68                Ok(ASV.to_string())
69            }
70            Self::EnglishedRevised => {
71                Ok(ERV.to_string())
72            }
73            Self::KingJames => {
74                Ok(KJV.to_string())
75            }
76            Self::Custom { path, .. } => {
77                // ensure the file exists
78                if !std::path::Path::new(path).exists() {
79                    return Err(BibleLibError::InvalidCustomTranslationFile);
80                }
81
82                // read the file and return the content
83                let result = std::fs::read_to_string(path);
84                match result {
85                    Ok(content) => Ok(content),
86                    Err(e) => Err(BibleLibError::IOError(e))
87                }
88            }
89        }
90    }
91}
92
93#[cfg(any(feature = "akjv", feature = "asv", feature = "erv", feature = "kjv"))]
94impl Default for Translation {
95    #[cfg(feature = "akjv")]
96    fn default() -> Self {
97        Self::AmericanKingJames
98    }
99    #[cfg(all(not(feature = "akjv"), feature = "asv"))]
100    fn default() -> Self {
101        Self::AmericanStandard
102    }
103    #[cfg(all(not(feature = "akjv"), not(feature = "asv"), feature = "erv"))]
104    fn default() -> Self {
105        Self::EnglishedRevised
106    }
107    #[cfg(all(not(feature = "akjv"), not(feature = "asv"), not(feature = "erv"), feature = "kjv"))]
108    fn default() -> Self {
109        Self::KingJames
110    }
111}
112
113impl Display for Translation {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            #[cfg(feature = "akjv")]
117            Self::AmericanKingJames => write!(f, "American King James Version"),
118            #[cfg(feature = "asv")]
119            Self::AmericanStandard => write!(f, "American Standard Version"),
120            #[cfg(feature = "erv")]
121            Self::EnglishedRevised => write!(f, "English Revised Version"),
122            #[cfg(feature = "kjv")]
123            Self::KingJames => write!(f, "King James Version"),
124            Self::Custom { name, .. } => write!(f, "Custom Translation: {}", name),
125        }
126    }
127}
128
129/// Struct representing a Bible verse lookup
130/// `book` is not case-sensitive
131/// `thru_verse` is optional and used for verse ranges like `John 3:16-18`
132/// # Example
133/// ```
134/// use bible_lib::{Bible, BibleLookup, Translation};
135///
136/// // get the bible translation
137/// let bible = Bible::new(Translation::KingJames).unwrap();
138/// // create a lookup for John 3:16
139/// let lookup = BibleLookup::new("John", 3, 16);
140/// // get the verse text
141/// let verse = bible.get_verse(lookup, false).unwrap();
142///
143/// // print the verse text
144/// println!("John 3:16: {}", verse);
145/// ```
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct BibleLookup {
148    pub book: String,
149    pub chapter: u32,
150    pub verse: u32,
151    pub thru_verse: Option<u32>,
152}
153
154impl BibleLookup {
155    /// Create a new BibleLookup instance (single verse)
156    /// `book` is not case-sensitive
157    /// # Example
158    /// ```
159    /// use bible_lib::BibleLookup;
160    ///
161    /// // create a lookup for John 3:16
162    /// let lookup = BibleLookup::new("John", 3, 16);
163    /// ```
164    pub fn new<S: Into<String>>(book: S, chapter: u32, verse: u32) -> Self {
165        let book = book.into();
166        let book = book.to_lowercase();
167        Self {
168            book,
169            chapter,
170            verse,
171            thru_verse: None,
172        }
173    }
174
175    /// Create a new BibleLookup instance (verse range)
176    /// # Example
177    /// ```
178    /// use bible_lib::BibleLookup;
179    ///
180    /// // create a lookup for Luke 23:39-43
181    /// let lookup = BibleLookup::new_range("Luke", 23, 39, 43);
182    /// ```
183    pub fn new_range<S: Into<String>>(book: S, chapter: u32, verse: u32, thru_verse: u32) -> Self {
184        let book = book.into();
185        let book = book.to_lowercase();
186        Self {
187            book,
188            chapter,
189            verse,
190            thru_verse: Some(thru_verse),
191        }
192    }
193
194    /// Detect Bible verses in a string
195    /// Requires the `detection` feature to be enabled
196    /// Can return multiple verses if more than one is found
197    /// # Example
198    /// ```
199    /// use bible_lib::{Bible, Translation, BibleLookup};
200    ///
201    /// // get the bible translation
202    /// let bible = Bible::new(Translation::default()).unwrap();
203    ///
204    /// // create the string to look for verses in
205    /// let text = "Show me John 3:16";
206    /// // detect verses in the string
207    /// let verses = BibleLookup::detect_from_string(text);
208    ///
209    /// // iterate through the found verses and print them
210    /// for verse in verses {
211    ///     // get the verse text
212    ///     let verse_text = bible.get_verse(verse.clone()).unwrap();
213    ///     // print the verse text
214    ///     println!("Found verse: {} - {}", verse, verse_text);
215    /// }
216    /// ```
217    #[cfg(feature = "detection")]
218    pub fn detect_from_string<S: Into<String>>(lookup: S) -> Vec<Self> {
219        let mut verses = Vec::new();
220
221        let lookup = lookup.into();
222        let text = lookup.to_lowercase();
223
224        //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();
225        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();
226        
227        for instance in regex.find_iter(&text) {
228            let instance = instance.as_str();
229            // to handle cases like `1 samuel` and `Song of Solomon`, split by ':' first and then split by whitespace
230            let mut parts = instance.split(':');
231            // split the first part by whitespace
232            let book_chapter = parts.next().unwrap().split_whitespace();
233            let count = book_chapter.clone().count();
234            let chapter = book_chapter.clone().last().unwrap().parse::<u32>().unwrap();
235            let book = book_chapter.take(count - 1).collect::<Vec<&str>>().join(" ").to_lowercase();
236
237            // handle cases where the verse is a range (i.e. `1-3`)
238            let verse_part = parts.next().unwrap();
239            if verse_part.contains('-') {
240                let verse_split = verse_part.split('-');
241                let verse = verse_split.clone().next().unwrap().parse::<u32>().unwrap();
242                let thru_verse = verse_split.clone().last().unwrap().parse::<u32>().unwrap();
243                verses.push(BibleLookup {
244                    book,
245                    chapter,
246                    verse,
247                    thru_verse: Some(thru_verse),
248                });
249            } else {
250                let verse = verse_part.parse::<u32>().unwrap();
251                verses.push(BibleLookup {
252                    book,
253                    chapter,
254                    verse,
255                    thru_verse: None,
256                });
257            }
258        }
259
260        verses
261    }
262
263    /// Capitalize the first letter of each word in the book name
264    /// Handles cases like `1 samuel` and `song of solomon`
265    /// This is used because book names are stored in lowercase for easier lookup
266    /// # Example
267    /// ```
268    /// use bible_lib::BibleLookup;
269    /// 
270    /// // capitalize book names
271    /// let book1 = BibleLookup::capitalize_book(&"john".to_string());
272    /// let book2 = BibleLookup::capitalize_book(&"1 samuel".to_string());
273    /// 
274    /// // print the capitalized book names
275    /// println!("Capitalized Book 1: {}", book1); // John
276    /// println!("Capitalized Book 2: {}", book2); // 1 Samuel
277    /// 
278    /// ```
279    pub fn capitalize_book(name: &String) -> String {
280        // capitalize the first letter of each word in the book name
281        // Split the input string by whitespace into words
282        name.split_whitespace()
283            // For each word, apply the following transformation
284            .map(|word| {
285                // Convert the word into characters
286                let mut chars = word.chars();
287                // If there's a first character, convert it to uppercase and concatenate it with the rest of the characters
288                if let Some(first_char) = chars.next() {
289                    if first_char.is_numeric() {
290                        // If the first character is numeric, leave it unchanged
291                        first_char.to_string() + &chars.collect::<String>()
292                    } else {
293                        // If the first character is not numeric, capitalize it
294                        first_char.to_uppercase().chain(chars).collect::<String>()
295                    }
296                } else {
297                    // If the word is empty, return an empty string
298                    String::new()
299                }
300            })
301            // Collect the transformed words back into a single string, separated by whitespace
302            .collect::<Vec<String>>().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!(f, "{} {}:{}-{}", Self::capitalize_book(&self.book), self.chapter, self.verse, thru_verse)
310        } else {
311            write!(f, "{} {}:{}", Self::capitalize_book(&self.book), self.chapter, self.verse)
312        }
313    }
314}
315
316/// Main Bible struct
317/// Stores the verses of the Bible for interfacing
318/// # Example
319/// ```
320/// use bible_lib::{Bible, Translation, BibleLookup};
321///
322/// // get the bible translation
323/// let bible = Bible::new(Translation::AmericanStandard).unwrap();
324///
325/// // create a lookup for John 3:16
326/// let lookup = BibleLookup::new("John", 3, 16);
327/// // get the verse text
328/// let verse = bible.get_verse(lookup, false).unwrap();
329///
330/// // print the verse text
331/// println!("John 3:16: {}", verse);
332/// ```
333#[derive(Debug, Clone)]
334pub struct Bible {
335    translation: Translation,
336    pub verses: HashMap<String /* Book */,
337                HashMap<u32 /* Chapter */,
338                HashMap<u32 /* Verse */, String /* Text */>>>,
339}
340
341impl Bible {
342
343    #[doc(hidden)]
344    fn parse_text(lines: &String) -> HashMap<String, HashMap<u32, HashMap<u32, String>>> {
345        let mut verses = HashMap::new();
346
347        for line in lines.lines() {
348            // to handle cases like `1 samuel` and `Song of Solomon`, split by ':' first and then split by whitespace
349            let mut parts = line.split(':');
350            // split the first part by whitespace
351            let book_chapter = parts.next().unwrap().split_whitespace();
352            let count = book_chapter.clone().count();
353            let chapter = book_chapter.clone().last().unwrap().parse::<u32>().unwrap();
354            let book = book_chapter.take(count - 1).collect::<Vec<&str>>().join(" ").to_lowercase();
355
356            let verse_text = parts.next().unwrap().split_whitespace();
357            let verse = verse_text.clone().next().unwrap().parse::<u32>().unwrap();
358            let text = verse_text.clone().skip(1).collect::<Vec<&str>>().join(" ");
359
360            if !verses.contains_key(&book) {
361                verses.insert(book.to_string(), HashMap::new());
362            }
363            if !verses.get_mut(&book).unwrap().contains_key(&chapter) {
364                verses.get_mut(&book).unwrap().insert(chapter, HashMap::new());
365            }
366            verses.get_mut(&book).unwrap().get_mut(&chapter).unwrap().insert(verse, text.to_string());
367        }
368
369        verses
370    }
371
372    /// Create a new Bible instance with the specified translation
373    pub fn new(translation: Translation) -> Result<Self, BibleLibError> {
374        let text = translation.get_text()?;
375        let verses = Self::parse_text(&text);
376        Ok(Self {
377            translation,
378            verses,
379        })
380    }
381
382    /// Get the current translation of the Bible instance
383    pub fn get_translation(&self) -> &Translation {
384        &self.translation
385    }
386
387    #[doc(hidden)]
388    fn replace_superscript(s: String) -> String {
389        s.chars().map(|c| {
390            match c {
391                '0' => '⁰',
392                '1' => '¹',
393                '2' => '²',
394                '3' => '³',
395                '4' => '⁴',
396                '5' => '⁵',
397                '6' => '⁶',
398                '7' => '⁷',
399                '8' => '⁸',
400                '9' => '⁹',
401                _ => c,
402            }
403        }).collect()
404    }
405
406    /// Get the text of a verse or range of verses
407    /// `use_superscripts` adds superscript verse numbers for better readability
408    /// Returns an error if the verse or chapter is not found
409    /// # Example
410    /// ```
411    /// use bible_lib::{Bible, BibleLookup, Translation};
412    ///
413    /// // get the bible translation
414    /// let bible = Bible::new(Translation::AmericanStandard).unwrap();
415    /// // create a lookup for John 3:16
416    /// let lookup = BibleLookup::new("John", 3, 16);
417    /// // get the verse text
418    /// let verse = bible.get_verse(lookup, false).unwrap();
419    ///
420    /// // print the verse text
421    /// println!("John 3:16: {}", verse);
422    /// ```
423    pub fn get_verse(&self, lookup: BibleLookup, use_superscripts: bool) -> Result<String, BibleLibError> {
424        // multiple verse lookup
425        if let Some(thru_verse) = lookup.thru_verse {
426            let mut verse_text = String::new();
427
428            // iterate through the verses
429            for verse in lookup.verse..=thru_verse {
430                let Some(chapters) = self.verses.get(&lookup.book) else {
431                    return Err(BibleLibError::BookNotFound);
432                };
433                let Some(verses) = chapters.get(&lookup.chapter) else {
434                    return Err(BibleLibError::ChapterNotFound);
435                };
436                let Some(text) = verses.get(&verse) else {
437                    return Err(BibleLibError::VerseNotFound);
438                };
439
440                if use_superscripts {
441                    verse_text.push_str(&format!("{}{} ", Self::replace_superscript(verse.to_string()), text));
442                } else {
443                    verse_text.push_str(text);
444                }
445            }
446            return Ok(verse_text.trim().to_string());
447        }
448        
449        // single verse lookup
450        let Some(chapters) = self.verses.get(&lookup.book) else {
451            return Err(BibleLibError::BookNotFound);
452        };
453        let Some(verses) = chapters.get(&lookup.chapter) else {
454            return Err(BibleLibError::ChapterNotFound);
455        };
456        let Some(text) = verses.get(&lookup.verse) else {
457            return Err(BibleLibError::VerseNotFound);
458        };
459
460        if use_superscripts {
461            Ok(format!("{}{}", Self::replace_superscript(lookup.verse.to_string()), text))
462        } else {
463            Ok(text.to_string())
464        }
465    }
466
467    /// Get the text of an entire chapter as a string
468    /// `use_superscripts` adds superscript verse numbers for better readability
469    /// Returns an error if the chapter is not found
470    /// # Example
471    /// ```
472    /// use bible_lib::{Bible, BibleLookup, Translation};
473    ///
474    /// // get the bible translation
475    /// let bible = Bible::new(Translation::EnglishedRevised).unwrap();
476    /// // get the text of Isaiah chapter 53
477    /// let chapter_text = bible.get_chapter("Isaiah", 53, true).unwrap();
478    ///
479    /// // print the chapter text
480    /// println!("Isaiah 53: {}", chapter_text);
481    /// ```
482    pub fn get_chapter(&self, book: &str, chapter: u32, use_superscripts: bool) -> Result<String, BibleLibError> {
483        let mut chapter_text = String::new();
484        // sort the verses by verse number
485        let Some(chapters) = self.verses.get(book) else {
486            return Err(BibleLibError::BookNotFound);
487        };
488        let Some(verses) = chapters.get(&chapter) else {
489            return Err(BibleLibError::ChapterNotFound);
490        };
491        let mut verses = verses.iter().collect::<Vec<(&u32, &String)>>();
492        verses.sort_by(|a, b| a.0.cmp(b.0));
493        for (verse, text) in verses {
494            let verse_designation = Self::replace_superscript(verse.to_string());
495            if use_superscripts {
496                chapter_text.push_str(&format!("{}{} ", verse_designation, text));
497            } else {
498                chapter_text.push_str(&format!("{} ", text));
499            }
500        }
501        Ok(chapter_text)
502    }
503
504    /// Get a list of all books in the Bible
505    /// # Example
506    /// ```
507    /// use bible_lib::{Bible, Translation};
508    ///
509    /// // get the bible translation
510    /// let bible = Bible::new(Translation::default()).unwrap();
511    ///
512    /// // get the list of books
513    /// let books = bible.get_books();
514    /// // print the list of books
515    /// println!("Books in the Bible: {:?}", books);
516    /// ```
517    pub fn get_books(&self) -> Vec<String> {
518        self.verses.keys().map(|s| s.to_string()).collect()
519    }
520
521    /// Get a list of all chapters in a book
522    /// # Example
523    /// ```
524    /// use bible_lib::{Bible, Translation};
525    ///
526    /// // get the bible translation
527    /// let bible = Bible::new(Translation::default()).unwrap();
528    ///
529    /// // get the list of chapters in Revelation
530    /// let chapters = bible.get_chapters("Revelation").unwrap();
531    /// // print the list of chapters
532    /// println!("Chapters in Revelation: {:?}", chapters);
533    /// ```
534    pub fn get_chapters(&self, book: &str) -> Result<Vec<u32>, BibleLibError> {
535        if let Some(chapters) = self.verses.get(book).map(|chapters| chapters.keys().map(|c| *c).collect()) {
536            Ok(chapters)
537        } else {
538            Err(BibleLibError::BookNotFound)
539        }
540    }
541
542    /// Get a list of all verses in a chapter of a book
543    /// # Example
544    /// ```
545    /// use bible_lib::{Bible, Translation};
546    ///
547    /// // get the bible translation
548    /// let bible = Bible::new(Translation::default()).unwrap();
549    ///
550    /// // get the list of verses in John chapter 3
551    /// let verses = bible.get_verses("John", 3).unwrap();
552    /// // print the list of verses
553    /// println!("Verses in John 3: {:?}", verses);
554    /// ```
555    pub fn get_verses(&self, book: &str, chapter: u32) -> Result<Vec<u32>, BibleLibError> {
556        if let Some(verses) = self.verses.get(book)
557            .and_then(|chapters| chapters.get(&chapter))
558            .map(|verses| verses.keys().map(|v| *v).collect()) {
559            Ok(verses)
560        } else {
561            Err(BibleLibError::ChapterNotFound)
562        }
563    }
564
565    /// Get the maximum verse number in a chapter of a book
566    pub fn get_max_verse(&self, book: &str, chapter: u32) -> Result<u32, BibleLibError> {
567        if let Some(verses) = self.verses.get(book)
568            .and_then(|chapters| chapters.get(&chapter)) {
569            if let Some(max_verse) = verses.keys().max() {
570                Ok(*max_verse)
571            } else {
572                Err(BibleLibError::ChapterNotFound)
573            }
574        } else {
575            Err(BibleLibError::ChapterNotFound)
576        }
577    }
578
579    /// Get a random verse from the Bible
580    /// Requires the `random` feature to be enabled
581    /// # Example
582    /// ```
583    /// use bible_lib::{Bible, Translation};
584    ///
585    /// // get the bible translation
586    /// let bible = Bible::new(Translation::default()).unwrap();
587    ///
588    /// // get a random verse
589    /// let random_verse = bible.random_verse();
590    /// // get the verse text
591    /// let verse_text = bible.get_verse(random_verse.clone(), false).unwrap();
592    /// // print the random verse
593    /// println!("Random Verse: {} - {}", random_verse, verse_text);
594    /// ```
595    #[cfg(feature = "random")]
596    pub fn random_verse(&self) -> BibleLookup {
597        use rand::seq::IteratorRandom;
598        let mut rng = rand::rng();
599        let book = self.verses.keys().choose(&mut rng).unwrap().to_string();
600        let chapters = self.verses.get(&book).unwrap();
601        let chapter = chapters.keys().choose(&mut rng).unwrap().to_owned();
602        let verses = chapters.get(&chapter).unwrap();
603        let verse = verses.keys().choose(&mut rng).unwrap().to_owned();
604        BibleLookup {
605            book,
606            chapter,
607            verse,
608            thru_verse: None,
609        }
610    }
611
612}