Skip to main content

bible_io_references/
book.rs

1//! Canonical Bible book names and standard machine-readable identifiers.
2
3use core::{fmt, str::FromStr};
4
5macro_rules! define_books {
6    (
7        $(
8            $variant:ident => {
9                abbreviation: $abbreviation:literal,
10                full_name: $full_name:literal,
11                osis: $osis:literal,
12                usfm: $usfm:literal,
13            }
14        ),+ $(,)?
15    ) => {
16        /// A Bible book supported by this crate.
17        ///
18        /// Variants are ordered canonically: the 66-book Protestant canon,
19        /// Catholic deuterocanonical books, then Eastern Orthodox additions.
20        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21        #[repr(u8)]
22        pub enum Book {
23            $(
24                #[doc = $full_name]
25                $variant
26            ),+
27        }
28
29        impl Book {
30            /// Every supported book in canonical order.
31            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
32
33            /// Return the package's compact, lowercase abbreviation.
34            #[must_use]
35            pub const fn abbreviation(self) -> &'static str {
36                match self {
37                    $(Self::$variant => $abbreviation),+
38                }
39            }
40
41            /// Return the canonical English display name.
42            #[must_use]
43            pub const fn full_name(self) -> &'static str {
44                match self {
45                    $(Self::$variant => $full_name),+
46                }
47            }
48
49            /// Return the case-sensitive OSIS book identifier.
50            #[must_use]
51            pub const fn osis(self) -> &'static str {
52                match self {
53                    $(Self::$variant => $osis),+
54                }
55            }
56
57            /// Return the case-sensitive USFM book identifier.
58            #[must_use]
59            pub const fn usfm(self) -> &'static str {
60                match self {
61                    $(Self::$variant => $usfm),+
62                }
63            }
64
65            /// Resolve an exact compact abbreviation, ignoring only ASCII case.
66            ///
67            /// Surrounding whitespace and alternate abbreviations are rejected.
68            #[must_use]
69            pub fn from_abbreviation(value: &str) -> Option<Self> {
70                Self::ALL
71                    .iter()
72                    .copied()
73                    .find(|book| book.abbreviation().eq_ignore_ascii_case(value))
74            }
75
76            /// Resolve an exact, case-sensitive OSIS book identifier.
77            #[must_use]
78            pub fn from_osis(value: &str) -> Option<Self> {
79                match value {
80                    $($osis => Some(Self::$variant),)+
81                    _ => None,
82                }
83            }
84
85            /// Resolve an exact, case-sensitive USFM book identifier.
86            #[must_use]
87            pub fn from_usfm(value: &str) -> Option<Self> {
88                match value {
89                    $($usfm => Some(Self::$variant),)+
90                    _ => None,
91                }
92            }
93
94            fn from_full_name(value: &str) -> Option<Self> {
95                Self::ALL
96                    .iter()
97                    .copied()
98                    .find(|book| book.full_name().eq_ignore_ascii_case(value))
99            }
100        }
101    };
102}
103
104define_books! {
105    Genesis => {
106        abbreviation: "gn",
107        full_name: "Genesis",
108        osis: "Gen",
109        usfm: "GEN",
110    },
111    Exodus => {
112        abbreviation: "ex",
113        full_name: "Exodus",
114        osis: "Exod",
115        usfm: "EXO",
116    },
117    Leviticus => {
118        abbreviation: "lv",
119        full_name: "Leviticus",
120        osis: "Lev",
121        usfm: "LEV",
122    },
123    Numbers => {
124        abbreviation: "nm",
125        full_name: "Numbers",
126        osis: "Num",
127        usfm: "NUM",
128    },
129    Deuteronomy => {
130        abbreviation: "dt",
131        full_name: "Deuteronomy",
132        osis: "Deut",
133        usfm: "DEU",
134    },
135    Joshua => {
136        abbreviation: "js",
137        full_name: "Joshua",
138        osis: "Josh",
139        usfm: "JOS",
140    },
141    Judges => {
142        abbreviation: "jud",
143        full_name: "Judges",
144        osis: "Judg",
145        usfm: "JDG",
146    },
147    Ruth => {
148        abbreviation: "rt",
149        full_name: "Ruth",
150        osis: "Ruth",
151        usfm: "RUT",
152    },
153    FirstSamuel => {
154        abbreviation: "1sm",
155        full_name: "1 Samuel",
156        osis: "1Sam",
157        usfm: "1SA",
158    },
159    SecondSamuel => {
160        abbreviation: "2sm",
161        full_name: "2 Samuel",
162        osis: "2Sam",
163        usfm: "2SA",
164    },
165    FirstKings => {
166        abbreviation: "1kgs",
167        full_name: "1 Kings",
168        osis: "1Kgs",
169        usfm: "1KI",
170    },
171    SecondKings => {
172        abbreviation: "2kgs",
173        full_name: "2 Kings",
174        osis: "2Kgs",
175        usfm: "2KI",
176    },
177    FirstChronicles => {
178        abbreviation: "1ch",
179        full_name: "1 Chronicles",
180        osis: "1Chr",
181        usfm: "1CH",
182    },
183    SecondChronicles => {
184        abbreviation: "2ch",
185        full_name: "2 Chronicles",
186        osis: "2Chr",
187        usfm: "2CH",
188    },
189    Ezra => {
190        abbreviation: "ezr",
191        full_name: "Ezra",
192        osis: "Ezra",
193        usfm: "EZR",
194    },
195    Nehemiah => {
196        abbreviation: "ne",
197        full_name: "Nehemiah",
198        osis: "Neh",
199        usfm: "NEH",
200    },
201    Esther => {
202        abbreviation: "et",
203        full_name: "Esther",
204        osis: "Esth",
205        usfm: "EST",
206    },
207    Job => {
208        abbreviation: "job",
209        full_name: "Job",
210        osis: "Job",
211        usfm: "JOB",
212    },
213    Psalms => {
214        abbreviation: "ps",
215        full_name: "Psalms",
216        osis: "Ps",
217        usfm: "PSA",
218    },
219    Proverbs => {
220        abbreviation: "prv",
221        full_name: "Proverbs",
222        osis: "Prov",
223        usfm: "PRO",
224    },
225    Ecclesiastes => {
226        abbreviation: "ec",
227        full_name: "Ecclesiastes",
228        osis: "Eccl",
229        usfm: "ECC",
230    },
231    SongOfSolomon => {
232        abbreviation: "so",
233        full_name: "Song of Solomon",
234        osis: "Song",
235        usfm: "SNG",
236    },
237    Isaiah => {
238        abbreviation: "is",
239        full_name: "Isaiah",
240        osis: "Isa",
241        usfm: "ISA",
242    },
243    Jeremiah => {
244        abbreviation: "jr",
245        full_name: "Jeremiah",
246        osis: "Jer",
247        usfm: "JER",
248    },
249    Lamentations => {
250        abbreviation: "lm",
251        full_name: "Lamentations",
252        osis: "Lam",
253        usfm: "LAM",
254    },
255    Ezekiel => {
256        abbreviation: "ez",
257        full_name: "Ezekiel",
258        osis: "Ezek",
259        usfm: "EZK",
260    },
261    Daniel => {
262        abbreviation: "dn",
263        full_name: "Daniel",
264        osis: "Dan",
265        usfm: "DAN",
266    },
267    Hosea => {
268        abbreviation: "ho",
269        full_name: "Hosea",
270        osis: "Hos",
271        usfm: "HOS",
272    },
273    Joel => {
274        abbreviation: "jl",
275        full_name: "Joel",
276        osis: "Joel",
277        usfm: "JOL",
278    },
279    Amos => {
280        abbreviation: "am",
281        full_name: "Amos",
282        osis: "Amos",
283        usfm: "AMO",
284    },
285    Obadiah => {
286        abbreviation: "ob",
287        full_name: "Obadiah",
288        osis: "Obad",
289        usfm: "OBA",
290    },
291    Jonah => {
292        abbreviation: "jn",
293        full_name: "Jonah",
294        osis: "Jonah",
295        usfm: "JON",
296    },
297    Micah => {
298        abbreviation: "mi",
299        full_name: "Micah",
300        osis: "Mic",
301        usfm: "MIC",
302    },
303    Nahum => {
304        abbreviation: "na",
305        full_name: "Nahum",
306        osis: "Nah",
307        usfm: "NAM",
308    },
309    Habakkuk => {
310        abbreviation: "hk",
311        full_name: "Habakkuk",
312        osis: "Hab",
313        usfm: "HAB",
314    },
315    Zephaniah => {
316        abbreviation: "zp",
317        full_name: "Zephaniah",
318        osis: "Zeph",
319        usfm: "ZEP",
320    },
321    Haggai => {
322        abbreviation: "hg",
323        full_name: "Haggai",
324        osis: "Hag",
325        usfm: "HAG",
326    },
327    Zechariah => {
328        abbreviation: "zc",
329        full_name: "Zechariah",
330        osis: "Zech",
331        usfm: "ZEC",
332    },
333    Malachi => {
334        abbreviation: "ml",
335        full_name: "Malachi",
336        osis: "Mal",
337        usfm: "MAL",
338    },
339    Matthew => {
340        abbreviation: "mt",
341        full_name: "Matthew",
342        osis: "Matt",
343        usfm: "MAT",
344    },
345    Mark => {
346        abbreviation: "mk",
347        full_name: "Mark",
348        osis: "Mark",
349        usfm: "MRK",
350    },
351    Luke => {
352        abbreviation: "lk",
353        full_name: "Luke",
354        osis: "Luke",
355        usfm: "LUK",
356    },
357    John => {
358        abbreviation: "jo",
359        full_name: "John",
360        osis: "John",
361        usfm: "JHN",
362    },
363    Acts => {
364        abbreviation: "act",
365        full_name: "Acts",
366        osis: "Acts",
367        usfm: "ACT",
368    },
369    Romans => {
370        abbreviation: "rm",
371        full_name: "Romans",
372        osis: "Rom",
373        usfm: "ROM",
374    },
375    FirstCorinthians => {
376        abbreviation: "1co",
377        full_name: "1 Corinthians",
378        osis: "1Cor",
379        usfm: "1CO",
380    },
381    SecondCorinthians => {
382        abbreviation: "2co",
383        full_name: "2 Corinthians",
384        osis: "2Cor",
385        usfm: "2CO",
386    },
387    Galatians => {
388        abbreviation: "gl",
389        full_name: "Galatians",
390        osis: "Gal",
391        usfm: "GAL",
392    },
393    Ephesians => {
394        abbreviation: "eph",
395        full_name: "Ephesians",
396        osis: "Eph",
397        usfm: "EPH",
398    },
399    Philippians => {
400        abbreviation: "ph",
401        full_name: "Philippians",
402        osis: "Phil",
403        usfm: "PHP",
404    },
405    Colossians => {
406        abbreviation: "cl",
407        full_name: "Colossians",
408        osis: "Col",
409        usfm: "COL",
410    },
411    FirstThessalonians => {
412        abbreviation: "1ts",
413        full_name: "1 Thessalonians",
414        osis: "1Thess",
415        usfm: "1TH",
416    },
417    SecondThessalonians => {
418        abbreviation: "2ts",
419        full_name: "2 Thessalonians",
420        osis: "2Thess",
421        usfm: "2TH",
422    },
423    FirstTimothy => {
424        abbreviation: "1tm",
425        full_name: "1 Timothy",
426        osis: "1Tim",
427        usfm: "1TI",
428    },
429    SecondTimothy => {
430        abbreviation: "2tm",
431        full_name: "2 Timothy",
432        osis: "2Tim",
433        usfm: "2TI",
434    },
435    Titus => {
436        abbreviation: "tt",
437        full_name: "Titus",
438        osis: "Titus",
439        usfm: "TIT",
440    },
441    Philemon => {
442        abbreviation: "phm",
443        full_name: "Philemon",
444        osis: "Phlm",
445        usfm: "PHM",
446    },
447    Hebrews => {
448        abbreviation: "hb",
449        full_name: "Hebrews",
450        osis: "Heb",
451        usfm: "HEB",
452    },
453    James => {
454        abbreviation: "jm",
455        full_name: "James",
456        osis: "Jas",
457        usfm: "JAS",
458    },
459    FirstPeter => {
460        abbreviation: "1pe",
461        full_name: "1 Peter",
462        osis: "1Pet",
463        usfm: "1PE",
464    },
465    SecondPeter => {
466        abbreviation: "2pe",
467        full_name: "2 Peter",
468        osis: "2Pet",
469        usfm: "2PE",
470    },
471    FirstJohn => {
472        abbreviation: "1jo",
473        full_name: "1 John",
474        osis: "1John",
475        usfm: "1JN",
476    },
477    SecondJohn => {
478        abbreviation: "2jo",
479        full_name: "2 John",
480        osis: "2John",
481        usfm: "2JN",
482    },
483    ThirdJohn => {
484        abbreviation: "3jo",
485        full_name: "3 John",
486        osis: "3John",
487        usfm: "3JN",
488    },
489    Jude => {
490        abbreviation: "jd",
491        full_name: "Jude",
492        osis: "Jude",
493        usfm: "JUD",
494    },
495    Revelation => {
496        abbreviation: "re",
497        full_name: "Revelation",
498        osis: "Rev",
499        usfm: "REV",
500    },
501    Tobit => {
502        abbreviation: "tb",
503        full_name: "Tobit",
504        osis: "Tob",
505        usfm: "TOB",
506    },
507    Judith => {
508        abbreviation: "jdt",
509        full_name: "Judith",
510        osis: "Jdt",
511        usfm: "JDT",
512    },
513    Wisdom => {
514        abbreviation: "ws",
515        full_name: "Wisdom",
516        osis: "Wis",
517        usfm: "WIS",
518    },
519    Sirach => {
520        abbreviation: "sir",
521        full_name: "Sirach",
522        osis: "Sir",
523        usfm: "SIR",
524    },
525    Baruch => {
526        abbreviation: "bar",
527        full_name: "Baruch",
528        osis: "Bar",
529        usfm: "BAR",
530    },
531    FirstMaccabees => {
532        abbreviation: "1mc",
533        full_name: "1 Maccabees",
534        osis: "1Macc",
535        usfm: "1MA",
536    },
537    SecondMaccabees => {
538        abbreviation: "2mc",
539        full_name: "2 Maccabees",
540        osis: "2Macc",
541        usfm: "2MA",
542    },
543    EstherAdditions => {
544        abbreviation: "etg",
545        full_name: "Esther (Greek)",
546        osis: "AddEsth",
547        usfm: "ADE",
548    },
549    DanielSongOfThree => {
550        abbreviation: "dn3",
551        full_name: "Daniel (Song of Three)",
552        osis: "PrAzar",
553        usfm: "S3Y",
554    },
555    DanielSusanna => {
556        abbreviation: "dns",
557        full_name: "Daniel (Susanna)",
558        osis: "Sus",
559        usfm: "SUS",
560    },
561    DanielBelAndTheDragon => {
562        abbreviation: "dnb",
563        full_name: "Daniel (Bel and the Dragon)",
564        osis: "Bel",
565        usfm: "BEL",
566    },
567    FirstEsdras => {
568        abbreviation: "1es",
569        full_name: "1 Esdras",
570        osis: "1Esd",
571        usfm: "1ES",
572    },
573    SecondEsdras => {
574        abbreviation: "2es",
575        full_name: "2 Esdras",
576        osis: "2Esd",
577        usfm: "2ES",
578    },
579    PrayerOfManasseh => {
580        abbreviation: "pmn",
581        full_name: "Prayer of Manasseh",
582        osis: "PrMan",
583        usfm: "MAN",
584    },
585    Psalm151 => {
586        abbreviation: "ps151",
587        full_name: "Psalm 151",
588        osis: "AddPs",
589        usfm: "PS2",
590    },
591    ThirdMaccabees => {
592        abbreviation: "3mc",
593        full_name: "3 Maccabees",
594        osis: "3Macc",
595        usfm: "3MA",
596    },
597    FourthMaccabees => {
598        abbreviation: "4mc",
599        full_name: "4 Maccabees",
600        osis: "4Macc",
601        usfm: "4MA",
602    },
603}
604
605impl fmt::Display for Book {
606    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
607        formatter.write_str(self.full_name())
608    }
609}
610
611/// Error returned when a string does not name a supported Bible book.
612#[derive(Clone, Debug, Eq, PartialEq)]
613pub struct ParseBookError {
614    input: String,
615}
616
617impl ParseBookError {
618    /// Return the rejected input.
619    #[must_use]
620    pub fn input(&self) -> &str {
621        &self.input
622    }
623}
624
625impl fmt::Display for ParseBookError {
626    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
627        write!(formatter, "unrecognized Bible book: {:?}", self.input)
628    }
629}
630
631impl std::error::Error for ParseBookError {}
632
633impl FromStr for Book {
634    type Err = ParseBookError;
635
636    fn from_str(value: &str) -> Result<Self, Self::Err> {
637        // Human-facing compact codes take precedence over machine formats.
638        // Strict OSIS/USFM callers should use their dedicated lookup APIs.
639        Self::from_abbreviation(value)
640            .or_else(|| Self::from_full_name(value))
641            .or_else(|| Self::from_osis(value))
642            .or_else(|| Self::from_usfm(value))
643            .ok_or_else(|| ParseBookError {
644                input: value.to_owned(),
645            })
646    }
647}
648
649#[cfg(test)]
650#[path = "../tests/unit/book.rs"]
651mod tests;