bib_unbound/
lib.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::fs::File;
4use std::hash::Hash;
5use std::io::{self, BufRead, BufReader};
6use std::num::NonZeroU8;
7use std::str::FromStr;
8
9/// A book of the Bible.
10///
11/// This includes the 66 books of the Protestant Bible, plus 20
12/// books/additions used in the Catholic and Eastern Orthodox traditions.
13// todo: better abbreviations?
14#[allow(non_camel_case_types)]
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
16pub enum Book {
17    /// Genesis
18    Gen,
19    /// Exodus
20    Ex,
21    /// Leviticus
22    Lev,
23    /// Numbers
24    Num,
25    /// Deuteronomy
26    Deut,
27    /// Joshua
28    Josh,
29    /// Judges
30    Judg,
31    /// Ruth
32    Ruth,
33    /// 1 Samuel
34    I_Sam,
35    /// 2 Samuel
36    II_Sam,
37    /// 1 Kings
38    I_Kings,
39    /// 2 Kings
40    II_Kings,
41    /// 1 Chronicles
42    I_Chr,
43    /// 2 Chronicles
44    II_Chr,
45    /// Ezra
46    Ezra,
47    /// Nehemiah
48    Neh,
49    /// Esther
50    Esth,
51    /// Job
52    Job,
53    /// Psalms
54    Ps,
55    /// Proverbs
56    Prov,
57    /// Ecclesiastes
58    Eccl,
59    /// Song of Solomon
60    Song,
61    /// Isaiah
62    Isa,
63    /// Jeremiah
64    Jer,
65    /// Lamentations
66    Lam,
67    /// Ezekiel
68    Ezek,
69    /// Daniel
70    Dan,
71    /// Hosea
72    Hos,
73    /// Joel
74    Joel,
75    /// Amos
76    Am,
77    /// Obadiah
78    Ob,
79    /// Jonah
80    Jon,
81    /// Micah
82    Mic,
83    /// Nahum
84    Nah,
85    /// Habakkuk
86    Hab,
87    /// Zephaniah
88    Zeph,
89    /// Haggai
90    Hag,
91    /// Zechariah
92    Zech,
93    /// Malachi
94    Mal,
95    /// Matthew
96    Mt,
97    /// Mark
98    Mk,
99    /// Luke
100    Lk,
101    /// John
102    Jn,
103    /// Acts
104    Acts,
105    /// Romans
106    Rom,
107    /// 1 Corinthians
108    I_Cor,
109    /// 2 Corinthians
110    II_Cor,
111    /// Galatians
112    Gal,
113    /// Ephesians
114    Eph,
115    /// Philippians
116    Phil,
117    /// Colossians
118    Col,
119    /// 1 Thessalonians
120    I_Thess,
121    /// 2 Thessalonians
122    II_Thess,
123    /// 1 Timothy
124    I_Tim,
125    /// 2 Timothy
126    II_Tim,
127    /// Titus
128    Titus,
129    /// Philemon
130    Philem,
131    /// Hebrews
132    Heb,
133    /// James
134    Jas,
135    /// 1 Peter
136    I_Pet,
137    /// 2 Peter
138    II_Pet,
139    /// 1 John
140    I_Jn,
141    /// 2 John
142    II_Jn,
143    /// 3 John
144    III_Jn,
145    /// Jude
146    Jude,
147    /// Revelation
148    Rev,
149    /// Tobit
150    Tob,
151    /// Judith
152    Jdt,
153    /// Greek additions to Esther
154    EsthGrk,
155    /// Wisdom of Solomon
156    Wis,
157    /// Sirach
158    Sir,
159    /// Baruch
160    Bar,
161    /// Letter of Jeremiah
162    LetJer,
163    /// Prayer of Azariah
164    SongOfThr,
165    /// Susanna
166    Sus,
167    /// Bel and the Dragon
168    Bel,
169    /// 1 Maccabees
170    I_Macc,
171    /// 2 Maccabees
172    II_Macc,
173    /// 3 Maccabees
174    III_Macc,
175    /// 4 Maccabees
176    IV_Macc,
177    /// 1 Esdras
178    I_Esd,
179    /// 2 Esdras
180    II_Esd,
181    /// Prayer of Manasses
182    PrMan,
183    /// Psalm 151
184    Ps151,
185    /// Psalm of Solomon
186    PsSol,
187    /// Odes
188    Odes,
189}
190
191impl Book {
192    fn new(s: &str) -> Option<Self> {
193        match s {
194            "01O" => Some(Self::Gen),
195            "02O" => Some(Self::Ex),
196            "03O" => Some(Self::Lev),
197            "04O" => Some(Self::Num),
198            "05O" => Some(Self::Deut),
199            "06O" => Some(Self::Josh),
200            "07O" => Some(Self::Judg),
201            "08O" => Some(Self::Ruth),
202            "09O" => Some(Self::I_Sam),
203            "10O" => Some(Self::II_Sam),
204            "11O" => Some(Self::I_Kings),
205            "12O" => Some(Self::II_Kings),
206            "13O" => Some(Self::I_Chr),
207            "14O" => Some(Self::II_Chr),
208            "15O" => Some(Self::Ezra),
209            "16O" => Some(Self::Neh),
210            "17O" => Some(Self::Esth),
211            "18O" => Some(Self::Job),
212            "19O" => Some(Self::Ps),
213            "20O" => Some(Self::Prov),
214            "21O" => Some(Self::Eccl),
215            "22O" => Some(Self::Song),
216            "23O" => Some(Self::Isa),
217            "24O" => Some(Self::Jer),
218            "25O" => Some(Self::Lam),
219            "26O" => Some(Self::Ezek),
220            "27O" => Some(Self::Dan),
221            "28O" => Some(Self::Hos),
222            "29O" => Some(Self::Joel),
223            "30O" => Some(Self::Am),
224            "31O" => Some(Self::Ob),
225            "32O" => Some(Self::Jon),
226            "33O" => Some(Self::Mic),
227            "34O" => Some(Self::Nah),
228            "35O" => Some(Self::Hab),
229            "36O" => Some(Self::Zeph),
230            "37O" => Some(Self::Hag),
231            "38O" => Some(Self::Zech),
232            "39O" => Some(Self::Mal),
233            "40N" => Some(Self::Mt),
234            "41N" => Some(Self::Mk),
235            "42N" => Some(Self::Lk),
236            "43N" => Some(Self::Jn),
237            "44N" => Some(Self::Acts),
238            "45N" => Some(Self::Rom),
239            "46N" => Some(Self::I_Cor),
240            "47N" => Some(Self::II_Cor),
241            "48N" => Some(Self::Gal),
242            "49N" => Some(Self::Eph),
243            "50N" => Some(Self::Phil),
244            "51N" => Some(Self::Col),
245            "52N" => Some(Self::I_Thess),
246            "53N" => Some(Self::II_Thess),
247            "54N" => Some(Self::I_Tim),
248            "55N" => Some(Self::II_Tim),
249            "56N" => Some(Self::Titus),
250            "57N" => Some(Self::Philem),
251            "58N" => Some(Self::Heb),
252            "59N" => Some(Self::Jas),
253            "60N" => Some(Self::I_Pet),
254            "61N" => Some(Self::II_Pet),
255            "62N" => Some(Self::I_Jn),
256            "63N" => Some(Self::II_Jn),
257            "64N" => Some(Self::III_Jn),
258            "65N" => Some(Self::Jude),
259            "66N" => Some(Self::Rev),
260            "67A" => Some(Self::Tob),
261            "68A" => Some(Self::Jdt),
262            "69A" => Some(Self::EsthGrk),
263            "70A" => Some(Self::Wis),
264            "71A" => Some(Self::Sir),
265            "72A" => Some(Self::Bar),
266            "73A" => Some(Self::LetJer),
267            "74A" => Some(Self::SongOfThr),
268            "75A" => Some(Self::Sus),
269            "76A" => Some(Self::Bel),
270            "77A" => Some(Self::I_Macc),
271            "78A" => Some(Self::II_Macc),
272            "79A" => Some(Self::III_Macc),
273            "80A" => Some(Self::IV_Macc),
274            "81A" => Some(Self::I_Esd),
275            "82A" => Some(Self::II_Esd),
276            "83A" => Some(Self::PrMan),
277            "84A" => Some(Self::Ps151),
278            "85A" => Some(Self::PsSol),
279            "86A" => Some(Self::Odes),
280            _ => None,
281        }
282    }
283
284    #[must_use]
285    pub const fn as_str(&self) -> &str {
286        match self {
287            Self::Gen => "Genesis",
288            Self::Ex => "Exodus",
289            Self::Lev => "Leviticus",
290            Self::Num => "Numbers",
291            Self::Deut => "Deuteronomy",
292            Self::Josh => "Joshua",
293            Self::Judg => "Judges",
294            Self::Ruth => "Ruth",
295            Self::I_Sam => "1 Samuel",
296            Self::II_Sam => "2 Samuel",
297            Self::I_Kings => "1 Kings",
298            Self::II_Kings => "2 Kings",
299            Self::I_Chr => "1 Chronicles",
300            Self::II_Chr => "2 Chronicles",
301            Self::Ezra => "Ezra",
302            Self::Neh => "Nehemiah",
303            Self::Esth => "Esther",
304            Self::Job => "Job",
305            Self::Ps => "Psalms",
306            Self::Prov => "Proverbs",
307            Self::Eccl => "Ecclesiastes",
308            Self::Song => "Song of Solomon",
309            Self::Isa => "Isaiah",
310            Self::Jer => "Jeremiah",
311            Self::Lam => "Lamentations",
312            Self::Ezek => "Ezekiel",
313            Self::Dan => "Daniel",
314            Self::Hos => "Hosea",
315            Self::Joel => "Joel",
316            Self::Am => "Amos",
317            Self::Ob => "Obadiah",
318            Self::Jon => "Jonah",
319            Self::Mic => "Micah",
320            Self::Nah => "Nahum",
321            Self::Hab => "Habakkuk",
322            Self::Zeph => "Zephaniah",
323            Self::Hag => "Haggai",
324            Self::Zech => "Zechariah",
325            Self::Mal => "Malachi",
326            Self::Mt => "Matthew",
327            Self::Mk => "Mark",
328            Self::Lk => "Luke",
329            Self::Jn => "John",
330            Self::Acts => "Acts",
331            Self::Rom => "Romans",
332            Self::I_Cor => "1 Corinthians",
333            Self::II_Cor => "2 Corinthians",
334            Self::Gal => "Galatians",
335            Self::Eph => "Ephesians",
336            Self::Phil => "Philippians",
337            Self::Col => "Colossians",
338            Self::I_Thess => "1 Thessalonians",
339            Self::II_Thess => "2 Thessalonians",
340            Self::I_Tim => "1 Timothy",
341            Self::II_Tim => "2 Timothy",
342            Self::Titus => "Titus",
343            Self::Philem => "Philemon",
344            Self::Heb => "Hebrews",
345            Self::Jas => "James",
346            Self::I_Pet => "1 Peter",
347            Self::II_Pet => "2 Peter",
348            Self::I_Jn => "1 John",
349            Self::II_Jn => "2 John",
350            Self::III_Jn => "3 John",
351            Self::Jude => "Jude",
352            Self::Rev => "Revelation",
353            Self::Tob => "Tobit",
354            Self::Jdt => "Judith",
355            Self::EsthGrk => "Esther, Greek",
356            Self::Wis => "Wisdom of Solomon",
357            Self::Sir => "Sirach",
358            Self::Bar => "Baruch",
359            Self::LetJer => "Epistle of Jeremiah",
360            Self::SongOfThr => "Prayer of Azariah",
361            Self::Sus => "Susanna",
362            Self::Bel => "Bel and the Dragon",
363            Self::I_Macc => "1 Maccabees",
364            Self::II_Macc => "2 Maccabees",
365            Self::III_Macc => "3 Maccabees",
366            Self::IV_Macc => "4 Maccabees",
367            Self::I_Esd => "1 Esdras",
368            Self::II_Esd => "2 Esdras",
369            Self::PrMan => "Prayer of Manasseh",
370            Self::Ps151 => "Psalm 151",
371            Self::PsSol => "Psalm of Solomon",
372            Self::Odes => "Odes",
373        }
374    }
375}
376
377impl fmt::Display for Book {
378    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
379        f.write_str(self.as_str())
380    }
381}
382
383/// The location of a single verse in the NRSV Bible, as a book, chapter, and verse number.
384#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
385pub struct PositionNrsv {
386    book: Book,
387    chap_no: u16,
388    vers_no: u16,
389}
390
391impl PositionNrsv {
392    /// The position's `Book`.
393    #[must_use]
394    pub const fn book(&self) -> Book {
395        self.book
396    }
397
398    /// The position's chapter number.
399    #[must_use]
400    pub const fn chap_no(&self) -> u16 {
401        self.chap_no
402    }
403
404    /// The position's verse number.
405    #[must_use]
406    pub const fn vers_no(&self) -> u16 {
407        self.vers_no
408    }
409}
410
411impl fmt::Display for PositionNrsv {
412    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
413        let Self { book, chap_no, vers_no } = *self;
414        if book == Book::Ps151 {
415            write!(f, "Psalm 151:{}", vers_no)
416        } else {
417            write!(f, "{} {}:{}", book, chap_no, vers_no)
418        }
419    }
420}
421
422#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
423enum PositionMeta {
424    None,
425    Subverse(NonZeroU8),
426    VerseRange(NonZeroU8),
427}
428
429impl PositionMeta {
430    fn new(s: &str) -> Option<Self> {
431        if s.is_empty() {
432            Some(Self::None)
433        } else if let Some(stripped) = s.strip_prefix('-') {
434            let x = NonZeroU8::from_str(stripped)
435                .unwrap_or_else(|_e| panic!("cannot parse {:?} as verse range", s));
436            Some(Self::VerseRange(x))
437        } else if let Some(stripped) = s.strip_prefix('.') {
438            let x = NonZeroU8::from_str(stripped)
439                .unwrap_or_else(|_e| panic!("cannot parse {:?} as subverse", s));
440            Some(Self::Subverse(x))
441        } else {
442            let x = match s {
443                "a" | "EndA" => 1,
444                "b" | "EndB" => 2,
445                "c" => 3,
446                "d" => 4,
447                "e" => 5,
448                "f" => 6,
449                "g" => 7,
450                "h" => 8,
451                "i" => 9,
452                "j" => 10,
453                "k" => 11,
454                "l" => 12,
455                "m" => 13,
456                "n" => 14,
457                "o" => 15,
458                "p" => 16,
459                "q" => 17,
460                "r" => 18,
461                "s" => 19,
462                "t" => 20,
463                "u" => 21,
464                "v" => 22,
465                "w" => 23,
466                "x" => 24,
467                "y" => 25,
468                "z" => 26,
469                "aa" => 27,
470                "bb" => 28,
471                "cc" => 29,
472                "dd" => 30,
473                "ee" => 31,
474                "ff" => 32,
475                "gg" => 33,
476                "hh" => 34,
477                "ii" => 35,
478                "jj" => 36,
479                "kk" => 37,
480                _ => return None,
481            };
482            Some(Self::Subverse(NonZeroU8::new(x).unwrap()))
483        }
484    }
485}
486
487impl fmt::Display for PositionMeta {
488    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
489        match self {
490            PositionMeta::Subverse(x) => write!(f, ".{x}"),
491            PositionMeta::VerseRange(x) => write!(f, "-{x}"),
492            PositionMeta::None => Ok(()),
493        }
494    }
495}
496
497/// The location of (roughly) verse-sized chunk of an arbitrary Bible translation.
498/// In the Unbound file format, these usually correspond to a single NRSV verse.
499///
500/// Unlike `PositionNRSV`, this type is translation-specific.
501/// Comparisons between `Position`s only make sense within a single translation.
502/// Otherwise, the behavior is what you would expect for `PositionNRSV`.
503#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
504pub struct Position {
505    book: Book,
506    chap_no: u16,
507    vers_no: u16,
508    meta: PositionMeta,
509}
510
511impl Position {
512    /// The position's `Book`.
513    #[must_use]
514    pub const fn book(&self) -> Book {
515        self.book
516    }
517
518    /// The position's chapter number.
519    #[must_use]
520    pub const fn chap_no(&self) -> u16 {
521        self.chap_no
522    }
523
524    /// The position's starting verse number.
525    ///
526    /// For a position representing a verse range, this is the starting verse number.
527    /// Otherwise, it is the same as `PositionNRSV`'s `vers_no`.
528    #[must_use]
529    pub const fn vers_beg(&self) -> u16 {
530        self.vers_no
531    }
532
533    /// The position's ending verse number.
534    ///
535    /// For a position representing a verse range, this is the verse number of the last verse in the range.
536    /// Otherwise, it is the same as `PositionNRSV`'s `vers_no`.
537    #[must_use]
538    pub const fn vers_end(&self) -> u16 {
539        let Self { vers_no, meta, .. } = *self;
540        let delta = if let PositionMeta::VerseRange(delta) = meta {
541            delta.get() as _
542        } else {
543            0
544        };
545        vers_no + delta
546    }
547
548    /// The position's subverse.
549    ///
550    /// For a position representing a subverse, this will return `Some(subverse)`.
551    /// Subverses are numbered `1` through `N`.
552    /// Currently, `N` does not appear to exceed `37` for any translation.
553    #[must_use]
554    pub const fn subverse(&self) -> Option<NonZeroU8> {
555        let Self { meta, .. } = *self;
556        if let PositionMeta::Subverse(x) = meta {
557            Some(x)
558        } else {
559            None
560        }
561    }
562}
563
564impl fmt::Display for Position {
565    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
566        let Self { book, chap_no, vers_no, meta } = *self;
567        let (book, chap_no) = if book == Book::Ps151 {
568            ("Psalm 151", 151)
569        } else {
570            (book.as_str(), chap_no)
571        };
572        write!(f, "{} {}:{}{}", book, chap_no, vers_no, meta)
573    }
574}
575
576impl PartialOrd for Position {
577    fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
578        let Self { book, chap_no, vers_no, meta } = *self;
579        let ordering = book
580            .cmp(&rhs.book)
581            .then(chap_no.cmp(&rhs.chap_no))
582            .then(vers_no.cmp(&rhs.vers_no));
583        match (meta, rhs.meta) {
584            (PositionMeta::None, PositionMeta::None) => Some(ordering),
585            (PositionMeta::Subverse(ref lhs), PositionMeta::Subverse(ref rhs))
586            | (PositionMeta::VerseRange(ref lhs), PositionMeta::VerseRange(ref rhs)) => {
587                Some(ordering.then(lhs.cmp(rhs)))
588            }
589            _ => None,
590        }
591    }
592}
593
594/// A single row from a file in the Unbound format.
595///
596/// For a translation that closely follows the NRSV numbering scheme,
597/// this is a single verse.
598/// Otherwise, it can possibly be a range of verses or a subverse.
599#[derive(Clone, Debug)]
600pub struct Verse {
601    pos_nrsv: Option<PositionNrsv>,
602    pos_orig: Position,
603    text: String,
604}
605
606impl Verse {
607    /// The text of the line or verse.
608    #[must_use]
609    pub fn text(&self) -> &str {
610        &*self.text
611    }
612
613    /// The position of the line or verse.
614    #[must_use]
615    pub const fn pos(&self) -> Position {
616        self.pos_orig
617    }
618
619    /// The corresponding position of the line or verse in the NRSV translation.
620    #[must_use]
621    pub const fn pos_nrsv(&self) -> Option<PositionNrsv> {
622        self.pos_nrsv
623    }
624}
625
626/// An interator over the lines of a file in the Unbound format.
627///
628/// For a translation that closely follows the NRSV numbering scheme,
629/// this yields individual verses.
630/// Otherwise, it can possibly yield ranges of verses or subverses,
631/// and it can yield the same verse multiple times,
632/// albeit with different native positions or NRSV positions.
633pub struct Verses<B>(io::Lines<B>);
634
635impl<B: BufRead> Iterator for Verses<B> {
636    type Item = io::Result<Verse>;
637
638    fn next(&mut self) -> Option<Self::Item> {
639        loop {
640            let row = match self.0.next()? {
641                Ok(row) => row,
642                Err(e) => return Some(Err(e)),
643            };
644            if row.trim().is_empty() || row.starts_with('#') {
645                continue;
646            }
647            let mut cols = row.split('\t');
648            let pos_nrsv = match (
649                cols.next().unwrap(),
650                cols.next().unwrap(),
651                cols.next().unwrap(),
652            ) {
653                ("", "", "") => None,
654                (x, y, z) => Some(PositionNrsv {
655                    book: Book::new(x).unwrap(),
656                    chap_no: u16::from_str(y).unwrap(),
657                    vers_no: u16::from_str(z).unwrap(),
658                }),
659            };
660            let (col4, col5, col6, col7, col8) = (
661                cols.next().unwrap(),
662                cols.next().unwrap(),
663                cols.next().unwrap(),
664                cols.next().unwrap(),
665                cols.next().unwrap(),
666            );
667            if Ok(0) == u8::from_str(col8) {
668                continue;
669            }
670            let pos_orig = Position {
671                book: Book::new(col4).unwrap(),
672                chap_no: u16::from_str(col5).unwrap(),
673                vers_no: u16::from_str(col6).unwrap(),
674                meta: PositionMeta::new(col7).unwrap(),
675            };
676            match cols.next() {
677                Some(x) if !x.is_empty() => {
678                    assert!(cols.next().is_none());
679                    return Some(Ok(Verse {
680                        pos_nrsv,
681                        pos_orig,
682                        text: x.into(),
683                    }));
684                }
685                _ => continue,
686            }
687        }
688    }
689}
690
691impl<B: BufRead> Verses<B> {
692    fn new(b: B) -> Self {
693        Self(b.lines())
694    }
695}
696
697/// Create an iterator over the lines of a string in the Unbound format.
698#[must_use]
699pub fn from_str(s: &str) -> Verses<&[u8]> {
700    Verses::new(s.as_ref())
701}
702
703/// Create an iterator over the lines of a file in the Unbound format.
704#[must_use]
705pub fn from_file(f: File) -> Verses<BufReader<File>> {
706    Verses::new(BufReader::new(f))
707}