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}