tytanic_utils/
fmt.rs

1//! Helper functions and types for formatting.
2
3use std::cell::RefCell;
4use std::fmt::Display;
5
6/// Types which affect the plurality of a word. Mostly numbers.
7pub trait Plural: Copy {
8    /// Returns whether a word representing this value is plural.
9    fn is_plural(self) -> bool;
10}
11
12macro_rules! impl_plural_num {
13    ($t:ty, $id:expr) => {
14        impl Plural for $t {
15            fn is_plural(self) -> bool {
16                self != $id
17            }
18        }
19    };
20}
21
22impl_plural_num!(u8, 1);
23impl_plural_num!(u16, 1);
24impl_plural_num!(u32, 1);
25impl_plural_num!(u64, 1);
26impl_plural_num!(u128, 1);
27impl_plural_num!(usize, 1);
28
29impl_plural_num!(i8, 1);
30impl_plural_num!(i16, 1);
31impl_plural_num!(i32, 1);
32impl_plural_num!(i64, 1);
33impl_plural_num!(i128, 1);
34impl_plural_num!(isize, 1);
35
36impl_plural_num!(f32, 1.0);
37impl_plural_num!(f64, 1.0);
38
39/// A struct which formats the given value in either singular (1) or plural
40/// (2+).
41///
42/// # Examples
43/// ```
44/// # use tytanic_utils::fmt::Term;
45/// assert_eq!(Term::simple("word").with(1).to_string(), "word");
46/// assert_eq!(Term::simple("word").with(2).to_string(), "words");
47/// assert_eq!(Term::new("index", "indices").with(1).to_string(), "index");
48/// assert_eq!(Term::new("index", "indices").with(2).to_string(), "indices");
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum Term<'a> {
52    /// Construct the plural term by appending an `s`.
53    Simple {
54        /// The singular term which can be turned into plural by appending an
55        /// `s`.
56        singular: &'a str,
57    },
58
59    /// Explicitly use the give singular and plural term.
60    Explicit {
61        /// The singular term.
62        singular: &'a str,
63
64        /// The plural term.
65        plural: &'a str,
66    },
67}
68
69impl<'a> Term<'a> {
70    /// Creates a new simple term whose plural term is created by appending an
71    /// `s`.
72    pub const fn simple(singular: &'a str) -> Self {
73        Self::Simple { singular }
74    }
75
76    /// Creates a term from the explicit singular and plural form.
77    pub const fn new(singular: &'a str, plural: &'a str) -> Self {
78        Self::Explicit { singular, plural }
79    }
80
81    /// Formats this term with the given value.
82    ///
83    /// # Examples
84    /// ```
85    /// # use tytanic_utils::fmt::Term;
86    /// assert_eq!(Term::simple("word").with(1).to_string(), "word");
87    /// assert_eq!(Term::simple("word").with(2).to_string(), "words");
88    /// assert_eq!(Term::new("index", "indices").with(1).to_string(), "index");
89    /// assert_eq!(Term::new("index", "indices").with(2).to_string(), "indices");
90    /// ```
91    pub fn with(self, plural: impl Plural) -> impl Display + 'a {
92        PluralDisplay {
93            terms: self,
94            is_plural: plural.is_plural(),
95        }
96    }
97}
98
99struct PluralDisplay<'a> {
100    terms: Term<'a>,
101    is_plural: bool,
102}
103
104impl Display for PluralDisplay<'_> {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match (&self.terms, self.is_plural) {
107            (Term::Simple { singular }, true) => write!(f, "{singular}s"),
108            (Term::Explicit { plural, .. }, true) => write!(f, "{plural}"),
109            (Term::Simple { singular }, false) => write!(f, "{singular}"),
110            (Term::Explicit { singular, .. }, false) => write!(f, "{singular}"),
111        }
112    }
113}
114
115/// Displays a sequence of elements as comma separated list with a final
116/// separator.
117///
118/// # Examples
119/// ```
120/// # use tytanic_utils::fmt::Separators;
121/// assert_eq!(
122///    Separators::new(", ", " or ").with(&["a", "b", "c"]).to_string(),
123///    "a, b or c",
124/// );
125/// assert_eq!(
126///    Separators::comma_or().with(&["a", "b", "c"]).to_string(),
127///    "a, b or c",
128/// );
129/// ```
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct Separators {
132    separator: &'static str,
133    terminal_separator: Option<&'static str>,
134}
135
136impl Separators {
137    /// Creates a new sequence to display.
138    ///
139    /// # Examples
140    /// ```
141    /// # use tytanic_utils::fmt::Separators;
142    /// assert_eq!(
143    ///     Separators::new("-", None).with(["a", "b", "c"]).to_string(),
144    ///     "a-b-c",
145    /// );
146    /// assert_eq!(
147    ///     Separators::new("-", "/").with(["a", "b", "c"]).to_string(),
148    ///     "a-b/c",
149    /// );
150    /// ```
151    pub fn new(
152        separator: &'static str,
153        terminal_separator: impl Into<Option<&'static str>>,
154    ) -> Self {
155        Self {
156            separator,
157            terminal_separator: terminal_separator.into(),
158        }
159    }
160
161    /// Creates a new sequence to display using only `, ` as separator.
162    ///
163    /// # Examples
164    /// ```
165    /// # use tytanic_utils::fmt::Separators;
166    /// assert_eq!(
167    ///    Separators::comma().with(["a", "b"]).to_string(),
168    ///    "a, b",
169    /// );
170    /// assert_eq!(
171    ///    Separators::comma().with(["a", "b", "c"]).to_string(),
172    ///    "a, b, c",
173    /// );
174    /// ```
175    pub fn comma() -> Self {
176        Self::new(", ", None)
177    }
178
179    /// Creates a new sequence to display using `, ` and ` or ` as the separators.
180    ///
181    /// # Examples
182    /// ```
183    /// # use tytanic_utils::fmt::Separators;
184    /// assert_eq!(
185    ///     Separators::comma_or().with(["a", "b"]).to_string(),
186    ///     "a or b",
187    /// );
188    /// assert_eq!(
189    ///     Separators::comma_or().with(["a", "b", "c"]).to_string(),
190    ///     "a, b or c",
191    /// );
192    /// ```
193    pub fn comma_or() -> Self {
194        Self::new(", ", " or ")
195    }
196
197    /// Creates a new sequence to display using `, ` and ` and ` as the separators.
198    ///
199    /// # Examples
200    /// ```
201    /// # use tytanic_utils::fmt::Separators;
202    /// assert_eq!(
203    ///    Separators::comma_and().with(["a", "b"]).to_string(),
204    ///    "a and b",
205    /// );
206    /// assert_eq!(
207    ///    Separators::comma_and().with(["a", "b", "c"]).to_string(),
208    ///    "a, b and c",
209    /// );
210    /// ```
211    pub fn comma_and() -> Self {
212        Self::new(", ", " and ")
213    }
214
215    // NOTE(tinger): this seems to take ages to type check in doc tests.
216
217    /// Formats th given items with this sequence's separators.
218    ///
219    /// # Examples
220    /// ```
221    /// # use tytanic_utils::fmt::Separators;
222    /// assert_eq!(
223    ///    Separators::new(", ", " or ").with(["a", "b", "c"]).to_string(),
224    ///    "a, b or c",
225    /// );
226    /// assert_eq!(
227    ///    Separators::comma_or().with(["a", "b", "c"]).to_string(),
228    ///    "a, b or c",
229    /// );
230    /// ```
231    pub fn with<S>(self, items: S) -> impl Display
232    where
233        S: IntoIterator,
234        S::IntoIter: ExactSizeIterator,
235        S::Item: Display,
236    {
237        SequenceDisplay {
238            seq: self,
239            items: RefCell::new(items.into_iter()),
240        }
241    }
242}
243
244struct SequenceDisplay<I> {
245    seq: Separators,
246    items: RefCell<I>,
247}
248
249impl<I> Display for SequenceDisplay<I>
250where
251    I: ExactSizeIterator,
252    I::Item: Display,
253{
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        let mut items = self.items.try_borrow_mut().expect("is not Sync");
256        let mut items = items.by_ref().enumerate();
257        let len = items.len();
258
259        if let Some((_, item)) = items.next() {
260            write!(f, "{item}")?;
261        } else {
262            return Ok(());
263        }
264
265        for (idx, item) in items {
266            let sep = if idx == len - 1 {
267                self.seq.terminal_separator.unwrap_or(self.seq.separator)
268            } else {
269                self.seq.separator
270            };
271
272            write!(f, "{sep}{item}")?;
273        }
274
275        Ok(())
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_term() {
285        assert_eq!(Term::simple("word").with(1).to_string(), "word");
286        assert_eq!(Term::simple("word").with(2).to_string(), "words");
287        assert_eq!(Term::new("index", "indices").with(1).to_string(), "index");
288        assert_eq!(Term::new("index", "indices").with(2).to_string(), "indices");
289    }
290
291    #[test]
292    fn test_separators() {
293        assert_eq!(
294            Separators::new(", ", " or ")
295                .with(["a", "b", "c"])
296                .to_string(),
297            "a, b or c",
298        );
299        assert_eq!(
300            Separators::comma_or().with(["a", "b", "c"]).to_string(),
301            "a, b or c",
302        );
303    }
304}