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}