Skip to main content

bible_io_references/
formatter.rs

1//! Localized, configurable formatting for references and passages.
2
3use core::fmt;
4
5use crate::{
6    book::Book,
7    language::Language,
8    language_data::{long_name, short_name},
9    passage::{BookPassage, ChapterPassage, Passage, PassageSequence, VersePassage},
10    reference::{Reference, VerseRange, VerseRef},
11};
12
13/// Controls whether localized book names are written in long or short form.
14#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
16pub enum BookNameStyle {
17    /// Use a localized full name, such as `Juan`.
18    #[default]
19    Long,
20    /// Use the shortest localized abbreviation, such as `Jn`.
21    Short,
22}
23
24/// Immutable options for localized reference and passage formatting.
25///
26/// The default formatter uses English long book names and compact ranges.
27/// A language without bundled names falls back to English deterministically.
28#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29pub struct ReferenceFormatter {
30    language: Language,
31    book_name_style: BookNameStyle,
32    compact_ranges: bool,
33}
34
35impl ReferenceFormatter {
36    /// The default English, long-name, compact-range formatter.
37    pub const DEFAULT: Self = Self::new();
38
39    /// Construct a formatter with the package defaults.
40    #[must_use]
41    pub const fn new() -> Self {
42        Self {
43            language: Language::English,
44            book_name_style: BookNameStyle::Long,
45            compact_ranges: true,
46        }
47    }
48
49    /// Construct a formatter from all configuration values.
50    #[must_use]
51    pub const fn with_options(
52        language: Language,
53        book_name_style: BookNameStyle,
54        compact_ranges: bool,
55    ) -> Self {
56        Self {
57            language,
58            book_name_style,
59            compact_ranges,
60        }
61    }
62
63    /// Construct a formatter for one language, retaining the other defaults.
64    #[must_use]
65    pub const fn for_language(language: Language) -> Self {
66        Self::new().with_language(language)
67    }
68
69    /// Return the requested output language.
70    #[must_use]
71    pub const fn language(self) -> Language {
72        self.language
73    }
74
75    /// Return the configured book-name style.
76    #[must_use]
77    pub const fn book_name_style(self) -> BookNameStyle {
78        self.book_name_style
79    }
80
81    /// Return whether repeated components are omitted from ranges and lists.
82    #[must_use]
83    pub const fn compact_ranges(self) -> bool {
84        self.compact_ranges
85    }
86
87    /// Return a copy configured for `language`.
88    #[must_use]
89    pub const fn with_language(mut self, language: Language) -> Self {
90        self.language = language;
91        self
92    }
93
94    /// Return a copy configured to use `book_name_style`.
95    #[must_use]
96    pub const fn with_book_name_style(mut self, book_name_style: BookNameStyle) -> Self {
97        self.book_name_style = book_name_style;
98        self
99    }
100
101    /// Return a copy configured to compact or expand repeated range components.
102    #[must_use]
103    pub const fn with_compact_ranges(mut self, compact_ranges: bool) -> Self {
104        self.compact_ranges = compact_ranges;
105        self
106    }
107
108    /// Return the preferred localized display name for `book`.
109    #[must_use]
110    pub fn book_name(self, book: Book) -> &'static str {
111        match self.book_name_style {
112            BookNameStyle::Long => long_name(book, self.language),
113            BookNameStyle::Short => short_name(book, self.language),
114        }
115    }
116
117    /// Alias for [`Self::book_name`] matching the Dart package terminology.
118    #[must_use]
119    pub fn format_book_name(self, book: Book) -> &'static str {
120        self.book_name(book)
121    }
122
123    /// Wrap a book in a lazily evaluated [`fmt::Display`] adapter.
124    #[must_use = "formatting adapters must be displayed or converted to a string"]
125    pub const fn format_book(self, book: Book) -> FormattedBook {
126        FormattedBook {
127            formatter: self,
128            book,
129        }
130    }
131
132    /// Wrap a verse or verse range in a localized [`fmt::Display`] adapter.
133    #[must_use = "formatting adapters must be displayed or converted to a string"]
134    pub fn format(self, reference: impl Into<Reference>) -> FormattedReference {
135        self.format_reference(reference)
136    }
137
138    /// Wrap a verse or verse range in a localized [`fmt::Display`] adapter.
139    #[must_use = "formatting adapters must be displayed or converted to a string"]
140    pub fn format_reference(self, reference: impl Into<Reference>) -> FormattedReference {
141        FormattedReference {
142            formatter: self,
143            reference: reference.into(),
144        }
145    }
146
147    /// Wrap a whole passage expression in a localized [`fmt::Display`] adapter.
148    #[must_use = "formatting adapters must be displayed or converted to a string"]
149    pub const fn format_passage<'a>(self, passage: &'a Passage) -> FormattedPassage<'a> {
150        FormattedPassage {
151            formatter: self,
152            passage,
153        }
154    }
155
156    /// Wrap a whole-book passage in a localized [`fmt::Display`] adapter.
157    #[must_use = "formatting adapters must be displayed or converted to a string"]
158    pub const fn format_book_passage(self, passage: BookPassage) -> FormattedBookPassage {
159        FormattedBookPassage {
160            formatter: self,
161            passage,
162        }
163    }
164
165    /// Wrap a chapter passage in a localized [`fmt::Display`] adapter.
166    #[must_use = "formatting adapters must be displayed or converted to a string"]
167    pub const fn format_chapter_passage(self, passage: ChapterPassage) -> FormattedChapterPassage {
168        FormattedChapterPassage {
169            formatter: self,
170            passage,
171        }
172    }
173
174    /// Wrap a verse-selection passage in a localized [`fmt::Display`] adapter.
175    #[must_use = "formatting adapters must be displayed or converted to a string"]
176    pub const fn format_verse_passage<'a>(
177        self,
178        passage: &'a VersePassage,
179    ) -> FormattedVersePassage<'a> {
180        FormattedVersePassage {
181            formatter: self,
182            passage,
183        }
184    }
185
186    /// Wrap a passage sequence in a localized [`fmt::Display`] adapter.
187    #[must_use = "formatting adapters must be displayed or converted to a string"]
188    pub const fn format_passage_sequence<'a>(
189        self,
190        passage: &'a PassageSequence,
191    ) -> FormattedPassageSequence<'a> {
192        FormattedPassageSequence {
193            formatter: self,
194            passage,
195        }
196    }
197
198    fn write_verse(self, output: &mut fmt::Formatter<'_>, verse: VerseRef) -> fmt::Result {
199        write!(
200            output,
201            "{} {}:{}",
202            self.book_name(verse.book()),
203            verse.chapter(),
204            verse.verse()
205        )
206    }
207
208    fn write_range(self, output: &mut fmt::Formatter<'_>, range: VerseRange) -> fmt::Result {
209        let start = range.start();
210        let end = range.end();
211
212        if !self.compact_ranges || start.book() != end.book() {
213            self.write_verse(output, start)?;
214            output.write_str("-")?;
215            return self.write_verse(output, end);
216        }
217
218        self.write_verse(output, start)?;
219        if start.chapter() == end.chapter() {
220            write!(output, "-{}", end.verse())
221        } else {
222            write!(output, "-{}:{}", end.chapter(), end.verse())
223        }
224    }
225
226    fn write_reference(self, output: &mut fmt::Formatter<'_>, reference: Reference) -> fmt::Result {
227        match reference {
228            Reference::Verse(verse) => self.write_verse(output, verse),
229            Reference::Range(range) => self.write_range(output, range),
230        }
231    }
232
233    fn write_chapter_passage(
234        self,
235        output: &mut fmt::Formatter<'_>,
236        passage: ChapterPassage,
237    ) -> fmt::Result {
238        write!(
239            output,
240            "{} {}",
241            self.book_name(passage.book()),
242            passage.start_chapter()
243        )?;
244        if let Some(end) = passage.end_chapter() {
245            write!(output, "-{end}")?;
246        }
247        Ok(())
248    }
249
250    fn write_verse_passage(
251        self,
252        output: &mut fmt::Formatter<'_>,
253        passage: &VersePassage,
254    ) -> fmt::Result {
255        let Some((first, rest)) = passage.selections().split_first() else {
256            return Ok(());
257        };
258
259        self.write_reference(output, *first)?;
260        if !self.compact_ranges || rest.is_empty() {
261            for selection in rest {
262                output.write_str(",")?;
263                self.write_reference(output, *selection)?;
264            }
265            return Ok(());
266        }
267
268        let anchor = first.start();
269        for selection in rest {
270            output.write_str(",")?;
271            self.write_relative_selection(output, *selection, anchor)?;
272        }
273        Ok(())
274    }
275
276    fn write_relative_selection(
277        self,
278        output: &mut fmt::Formatter<'_>,
279        selection: Reference,
280        anchor: VerseRef,
281    ) -> fmt::Result {
282        match selection {
283            Reference::Verse(verse) => self.write_relative_verse(output, verse, anchor),
284            Reference::Range(range) => self.write_relative_range(output, range, anchor),
285        }
286    }
287
288    fn write_relative_verse(
289        self,
290        output: &mut fmt::Formatter<'_>,
291        verse: VerseRef,
292        anchor: VerseRef,
293    ) -> fmt::Result {
294        if verse.book() != anchor.book() {
295            return self.write_verse(output, verse);
296        }
297        if verse.chapter() == anchor.chapter() {
298            write!(output, "{}", verse.verse())
299        } else {
300            write!(output, "{}:{}", verse.chapter(), verse.verse())
301        }
302    }
303
304    fn write_relative_range(
305        self,
306        output: &mut fmt::Formatter<'_>,
307        range: VerseRange,
308        anchor: VerseRef,
309    ) -> fmt::Result {
310        let start = range.start();
311        let end = range.end();
312        if start.book() != anchor.book() || end.book() != anchor.book() {
313            return self.write_range(output, range);
314        }
315
316        if start.chapter() == anchor.chapter() {
317            write!(output, "{}", start.verse())?;
318        } else {
319            write!(output, "{}:{}", start.chapter(), start.verse())?;
320        }
321        output.write_str("-")?;
322        if start.chapter() == end.chapter() {
323            write!(output, "{}", end.verse())
324        } else {
325            write!(output, "{}:{}", end.chapter(), end.verse())
326        }
327    }
328
329    fn write_passage(self, output: &mut fmt::Formatter<'_>, passage: &Passage) -> fmt::Result {
330        match passage {
331            Passage::Book(passage) => output.write_str(self.book_name(passage.book())),
332            Passage::Chapter(passage) => self.write_chapter_passage(output, *passage),
333            Passage::Verses(passage) => self.write_verse_passage(output, passage),
334            Passage::Sequence(sequence) => self.write_passage_sequence(output, sequence),
335        }
336    }
337
338    fn write_passage_sequence(
339        self,
340        output: &mut fmt::Formatter<'_>,
341        sequence: &PassageSequence,
342    ) -> fmt::Result {
343        let mut separator = "";
344        for passage in sequence.passages() {
345            output.write_str(separator)?;
346            self.write_passage(output, passage)?;
347            separator = "; ";
348        }
349        Ok(())
350    }
351}
352
353impl Default for ReferenceFormatter {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359impl From<Language> for ReferenceFormatter {
360    fn from(language: Language) -> Self {
361        Self::for_language(language)
362    }
363}
364
365/// A lazily formatted, localized book name.
366#[derive(Clone, Copy, Debug, Eq, PartialEq)]
367pub struct FormattedBook {
368    formatter: ReferenceFormatter,
369    book: Book,
370}
371
372impl fmt::Display for FormattedBook {
373    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
374        output.write_str(self.formatter.book_name(self.book))
375    }
376}
377
378/// A lazily formatted, localized verse or verse range.
379#[derive(Clone, Copy, Debug, Eq, PartialEq)]
380pub struct FormattedReference {
381    formatter: ReferenceFormatter,
382    reference: Reference,
383}
384
385impl fmt::Display for FormattedReference {
386    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
387        self.formatter.write_reference(output, self.reference)
388    }
389}
390
391/// A lazily formatted, localized whole passage expression.
392#[derive(Clone, Copy, Debug)]
393pub struct FormattedPassage<'a> {
394    formatter: ReferenceFormatter,
395    passage: &'a Passage,
396}
397
398impl fmt::Display for FormattedPassage<'_> {
399    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
400        self.formatter.write_passage(output, self.passage)
401    }
402}
403
404/// A lazily formatted, localized whole-book passage.
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub struct FormattedBookPassage {
407    formatter: ReferenceFormatter,
408    passage: BookPassage,
409}
410
411impl fmt::Display for FormattedBookPassage {
412    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
413        output.write_str(self.formatter.book_name(self.passage.book()))
414    }
415}
416
417/// A lazily formatted, localized chapter passage.
418#[derive(Clone, Copy, Debug, Eq, PartialEq)]
419pub struct FormattedChapterPassage {
420    formatter: ReferenceFormatter,
421    passage: ChapterPassage,
422}
423
424impl fmt::Display for FormattedChapterPassage {
425    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
426        self.formatter.write_chapter_passage(output, self.passage)
427    }
428}
429
430/// A lazily formatted, localized verse-selection passage.
431#[derive(Clone, Copy, Debug)]
432pub struct FormattedVersePassage<'a> {
433    formatter: ReferenceFormatter,
434    passage: &'a VersePassage,
435}
436
437impl fmt::Display for FormattedVersePassage<'_> {
438    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
439        self.formatter.write_verse_passage(output, self.passage)
440    }
441}
442
443/// A lazily formatted, localized passage sequence.
444#[derive(Clone, Copy, Debug)]
445pub struct FormattedPassageSequence<'a> {
446    formatter: ReferenceFormatter,
447    passage: &'a PassageSequence,
448}
449
450impl fmt::Display for FormattedPassageSequence<'_> {
451    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
452        self.formatter.write_passage_sequence(output, self.passage)
453    }
454}
455
456#[cfg(test)]
457#[path = "../tests/unit/formatter.rs"]
458mod tests;