bible/src/texts/bible.rs
Oliver Atkinson 09acb5af50 working
2025-03-26 13:46:51 -06:00

288 lines
7.4 KiB
Rust

use inline_colorization::*;
use std::{fmt::Display, fs, path::PathBuf};
use quick_xml::de::from_str;
use serde::Deserialize;
pub const BOOKS_IN_ORDER: [&str; 66] = [
"Genesis",
"Exodus",
"Leviticus",
"Numbers",
"Deuteronomy",
"Joshua",
"Judges",
"Ruth",
"1 Samuel",
"2 Samuel",
"1 Kings",
"2 Kings",
"1 Chronicles",
"2 Chronicles",
"Ezra",
"Nehemiah",
"Esther",
"Job",
"Psalms",
"Proverbs",
"Ecclesiastes",
"Song of Solomon",
"Isaiah",
"Jeremiah",
"Lamentations",
"Ezekiel",
"Daniel",
"hosea",
"Joel",
"Amos",
"Obadiah",
"Jonah",
"Micah",
"Nahum",
"Habakkuk",
"Zephaniah",
"Haggai",
"Zechariah",
"Malachi",
"Matthew",
"Mark",
"Luke",
"John",
"Acts",
"Romans",
"1 Corinthians",
"2 Corinthians",
"Galations",
"Ephesians",
"Philippians",
"Colossians",
"1 Thessalonians",
"2 Thessalonians",
"1 Timothy",
"2 Timothy",
"Titus",
"Philemon",
"Hebrews",
"James",
"1 Peter",
"2 Peter",
"1 John",
"2 John",
"3 John",
"Jude",
"Revelation",
];
pub fn get(query: &str, location: &str, files: Vec<PathBuf>) {
// expecting query to be in format:
// Gen 1:2
// Gen 1:2-5
// Gen 1
// The book name needs to be just long enough to be unique
let mut splits = location.split(':');
let chapter = splits.next();
let verse = splits.next();
// ###########################################
// Figure out what book they are talking about
let res = BOOKS_IN_ORDER
.iter()
.enumerate()
.filter(|(_, book)| book.to_lowercase().contains(&query.to_lowercase()))
.collect::<Vec<(usize, &&str)>>();
let (book_idx, book_name) = match res.len() {
1 => res[0],
2.. => {
eprintln!("Err: Ambigious input '{query}', could be any of:");
for (_, i) in &res {
eprintln!("\t{i}");
}
return;
}
_ => {
eprintln!("'{query}' is not a book");
return;
}
};
// Load Bibles into memory
let bibles = files
.iter()
.filter_map(|path| fs::read_to_string(path).ok())
.filter_map(|contents| from_str::<Bible>(&contents).ok())
.collect::<Vec<Bible>>();
// Select the book in each Bible
let books = bibles
.iter()
// Book are 1 indexed in the xml spec
.map(|bible| (bible, bible.get_book_by_index(book_idx + 1)))
.filter(|(_,e)| e.is_some())
.map(|(bible,book)| (bible,book.unwrap()))
.collect::<Vec<(&Bible,&Book)>>();
// Select the chapter in each Bible
let chapters = if let Some(chapter) = chapter {
if let Ok(ch_num) = chapter.parse::<usize>() {
books
.iter()
.map(|(bible,book)| (bible, book, book.get_chapter_by_index(ch_num)))
.filter(|(_,_,book)| book.is_some())
.map(|(bible,book,chapter)| (*bible,*book,chapter.unwrap()))
.collect::<Vec<(&Bible, &Book, &Chapter)>>()
} else {
return;
}
} else {
return;
};
// Get the verse in each Bible
match verse {
Some(verse) => {
let mut splits = verse.split("-");
let start = splits.next();
let end = splits.next();
match (start, end) {
// range of verses
(Some(sn), Some(en)) => {
if let (Ok(start), Ok(end)) = (sn.parse::<usize>(), en.parse::<usize>()) {
for num in start..=end {
for (bible, _book, chapter) in &chapters {
if let Some(verse) = chapter.get_verse_by_index(num) {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}:{}{style_reset}: {verse}",
bible.translation_name, chapter.number, verse.number
);
}
}
}
}
}
// only one verse
(Some(ver_num), None) => {
if let Ok(num) = ver_num.parse::<usize>() {
for (bible, _book, chapter) in chapters {
if let Some(verse) = chapter.get_verse_by_index(num) {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}:{}{style_reset}: {verse}",
bible.translation_name, chapter.number, verse.number
);
}
}
}
}
_ => {
// couldn't parse verse
return;
}
}
}
None => {
// only chapter
for (bible, _book, chapter) in chapters {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}{style_reset}:",
bible.translation_name, chapter.number
);
print!("{}", chapter);
}
}
}
}
impl Bible {
fn get_book_by_index(&self, idx: usize) -> Option<&Book> {
for i in &self.testaments {
for book in &i.books {
if book.number == idx {
return Some(&book);
}
}
}
None
}
}
impl Book {
fn get_chapter_by_index(&self, idx: usize) -> Option<&Chapter> {
for chap in &self.chapters {
if chap.number == idx {
return Some(&chap);
}
}
None
}
}
impl Chapter {
fn get_verse_by_index(&self, idx: usize) -> Option<&Verse> {
for verse in &self.verses {
if verse.number == idx {
return Some(&verse);
}
}
None
}
}
#[derive(Deserialize)]
struct Bible {
#[serde(rename = "@translation")]
translation_name: String,
#[serde(rename = "testament")]
testaments: Vec<Testament>,
}
#[derive(Deserialize)]
struct Testament {
#[serde(rename = "@name")]
// yes, this would make a nice enum
_testament: String,
#[serde(rename = "book")]
books: Vec<Book>,
}
#[derive(Deserialize)]
struct Book {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "chapter")]
chapters: Vec<Chapter>,
}
#[derive(Deserialize)]
struct Chapter {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "verse")]
verses: Vec<Verse>,
}
impl Display for Chapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for v in &self.verses {
writeln!(
f,
"{style_bold}{style_underline}{}{style_reset}: {}",
v.number, v
)?;
}
Ok(())
}
}
#[derive(Deserialize)]
struct Verse {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "$text")]
text: String,
}
impl Display for Verse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}