joinery/
join.rs

1//! Core join type and related traits
2
3use core::fmt::{self, Display, Formatter};
4
5#[cfg(feature = "token-stream")]
6use {proc_macro2::TokenStream, quote::ToTokens};
7
8use crate::{
9    iter::{JoinItem, JoinIter, JoinableIterator},
10    separators::NoSeparator,
11};
12
13/// A trait for converting collections into [`Join`] instances.
14///
15/// This trait is implemented for all referentially iterable types; that is,
16/// for all types for which `&T: IntoIterator`. See [`join_with`][Joinable::join_with]
17/// for an example of its usage.
18pub trait Joinable: Sized {
19    type Collection;
20    /// Combine this object with a separator to create a new [`Join`] instance.
21    /// Note that the separator does not have to share the same type as the
22    /// iterator's values.
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// use joinery::Joinable;
28    ///
29    /// let parts = vec!["this", "is", "a", "sentence"];
30    /// let join = parts.join_with(' ');
31    /// assert_eq!(join.to_string(), "this is a sentence");
32    /// ```
33    fn join_with<S>(self, sep: S) -> Join<Self::Collection, S>;
34
35    /// Join this object with an [empty separator](NoSeparator). When rendered
36    /// with [`Display`], the underlying elements will be directly concatenated.
37    /// Note that the separator, while empty, is still present, and will show
38    /// up if you iterate this instance.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use joinery::Joinable;
44    ///
45    /// let parts = vec!['a', 'b', 'c', 'd', 'e'];
46    /// let join = parts.join_concat();
47    /// assert_eq!(join.to_string(), "abcde");
48    /// ```
49    fn join_concat(self) -> Join<Self::Collection, NoSeparator> {
50        self.join_with(NoSeparator)
51    }
52}
53
54impl<T> Joinable for T
55where
56    for<'a> &'a T: IntoIterator,
57{
58    type Collection = Self;
59
60    fn join_with<S>(self, sep: S) -> Join<Self, S> {
61        Join {
62            collection: self,
63            sep,
64        }
65    }
66}
67
68/// A trait for using a separator to produce a [`Join`].
69///
70/// This trait provides a more python-style interface for performing joins.
71/// Rather use [`collection.join_with`][Joinable::join_with], you can instead
72/// use:
73///
74/// ```
75/// use joinery::Separator;
76///
77/// let join = ", ".separate([1, 2, 3, 4]);
78/// assert_eq!(join.to_string(), "1, 2, 3, 4");
79/// ```
80///
81/// By default, [`Separator`] is implemented for [`char`] and [`&str`][str], as
82/// well as all the separator types in `separators`.
83///
84/// Note that any type can be used as a separator in a [`Join`] when
85/// creating one via [`Joinable::join_with`]. The [`Separator`] trait and its
86/// implementations on [`char`] and [`&str`][str] are provided simply as
87/// a convenience.
88pub trait Separator: Sized {
89    fn separate<T: Joinable>(self, collection: T) -> Join<T::Collection, Self> {
90        collection.join_with(self)
91    }
92}
93
94impl Separator for char {}
95impl<'a> Separator for &'a str {}
96
97/// The primary data structure for representing a joined sequence.
98///
99/// It contains a collection and a separator, and represents the elements of the
100/// collection with the separator dividing each element. A collection is defined
101/// as any type for which `&T: IntoIterator`; that is, any time for which references
102/// to the type are iterable.
103///
104/// A [`Join`] is created with [`Joinable::join_with`], [`Separator::separate`], or
105/// [`JoinableIterator::join_with`]. Its main use is an implementation of [`Display`],
106/// which writes out the elements of the underlying collection, separated by the
107/// separator. It also implements [`IntoIterator`], using a [`JoinIter`].
108///
109/// # Examples
110///
111/// Writing via [`Display`]:
112///
113/// ```
114/// use joinery::Joinable;
115/// use std::fmt::Write;
116///
117/// let content = [1, 2, 3, 4, 5, 6, 7, 8, 9];
118/// let join = content.join_with(", ");
119///
120/// let mut buffer = String::new();
121/// write!(buffer, "Numbers: {}", join);
122///
123/// assert_eq!(buffer, "Numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9");
124///
125/// // Don't forget that `Display` gives to `ToString` for free!
126/// assert_eq!(join.to_string(), "1, 2, 3, 4, 5, 6, 7, 8, 9")
127/// ```
128///
129/// Iterating via [`IntoIterator`]:
130///
131/// ```
132/// use joinery::{Separator, JoinItem};
133///
134/// let content = [0, 1, 2];
135/// let join = ", ".separate(content);
136/// let mut join_iter = (&join).into_iter();
137///
138/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&0)));
139/// assert_eq!(join_iter.next(), Some(JoinItem::Separator(&", ")));
140/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&1)));
141/// assert_eq!(join_iter.next(), Some(JoinItem::Separator(&", ")));
142/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&2)));
143/// assert_eq!(join_iter.next(), None);
144/// ```
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[must_use]
147pub struct Join<C, S> {
148    collection: C,
149    sep: S,
150}
151
152impl<C, S> Join<C, S> {
153    /// Get a reference to the separator.
154    pub fn sep(&self) -> &S {
155        &self.sep
156    }
157
158    /// Get a reference to the underlying collection.
159    pub fn collection(&self) -> &C {
160        &self.collection
161    }
162
163    /// Get a mutable reference to the underlying collection
164    pub fn collection_mut(&mut self) -> &mut C {
165        &mut self.collection
166    }
167
168    /// Consume the join, and return the underlying collection.
169    pub fn into_collection(self) -> C {
170        self.collection
171    }
172
173    /// Consume `self` and return underlying collection and separator.
174    pub fn into_parts(self) -> (C, S) {
175        (self.collection, self.sep)
176    }
177}
178
179impl<C, S: Display> Display for Join<C, S>
180where
181    for<'a> &'a C: IntoIterator,
182    for<'a> <&'a C as IntoIterator>::Item: Display,
183{
184    /// Format the joined collection, by writing out each element of the
185    /// collection, separated by the separator.
186    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
187        let mut iter = self.collection.into_iter();
188
189        match iter.next() {
190            None => Ok(()),
191            Some(first) => {
192                first.fmt(f)?;
193
194                iter.try_for_each(move |element| {
195                    self.sep.fmt(f)?;
196                    element.fmt(f)
197                })
198            }
199        }
200    }
201}
202
203impl<C: IntoIterator, S: Clone> IntoIterator for Join<C, S> {
204    type IntoIter = JoinIter<C::IntoIter, S>;
205    type Item = JoinItem<C::Item, S>;
206
207    /// Create a consuming iterator from a `Join`. This iterator consumes the
208    /// elements of the underlying collection, and intersperses them with clones
209    /// of the separator. See the [`JoinIter`] documentation for more details.
210    fn into_iter(self) -> Self::IntoIter {
211        self.collection.into_iter().iter_join_with(self.sep)
212    }
213}
214
215impl<'a, C, S> IntoIterator for &'a Join<C, S>
216where
217    &'a C: IntoIterator,
218{
219    type IntoIter = JoinIter<<&'a C as IntoIterator>::IntoIter, &'a S>;
220    type Item = JoinItem<<&'a C as IntoIterator>::Item, &'a S>;
221
222    /// Create a referential iterator over the join. This iterator iterates
223    /// over references to the underlying collection, interspersed with references
224    /// to the separator. See the [`JoinIter`] documentation for more details.
225    fn into_iter(self) -> Self::IntoIter {
226        self.collection.into_iter().iter_join_with(&self.sep)
227    }
228}
229
230#[cfg(feature = "token-stream")]
231impl<C, S> ToTokens for Join<C, S>
232where
233    for<'a> &'a C: IntoIterator,
234    for<'a> <&'a C as IntoIterator>::Item: ToTokens,
235    S: ToTokens,
236{
237    fn to_tokens(&self, tokens: &mut TokenStream) {
238        let mut iter = self.collection.into_iter();
239        if let Some(first) = iter.next() {
240            first.to_tokens(tokens);
241
242            iter.for_each(move |item| {
243                self.sep.to_tokens(tokens);
244                item.to_tokens(tokens);
245            })
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252
253    use super::{Joinable, Separator};
254
255    #[test]
256    fn empty() {
257        let data: Vec<String> = Vec::new();
258        let join = data.join_with(", ");
259        let result = join.to_string();
260
261        assert_eq!(result, "");
262    }
263
264    #[test]
265    fn single() {
266        let data = vec!["Hello"];
267        let join = data.join_with(", ");
268        let result = join.to_string();
269
270        assert_eq!(result, "Hello");
271    }
272
273    #[test]
274    fn simple_join() {
275        let data = vec!["This", "is", "a", "sentence"];
276        let join = data.join_with(' ');
277        let result = join.to_string();
278
279        assert_eq!(result, "This is a sentence");
280    }
281
282    #[test]
283    fn join_via_separator() {
284        let data = vec!["This", "is", "a", "sentence"];
285        let join = ' '.separate(data);
286        let result = join.to_string();
287
288        assert_eq!(result, "This is a sentence");
289    }
290
291    #[test]
292    fn iter() {
293        use crate::iter::JoinItem;
294
295        let data = vec!["Hello", "World"];
296        let join = data.join_with(' ');
297        let mut iter = join.into_iter();
298
299        assert_eq!(iter.next(), Some(JoinItem::Element("Hello")));
300        assert_eq!(iter.next(), Some(JoinItem::Separator(' ')));
301        assert_eq!(iter.next(), Some(JoinItem::Element("World")));
302        assert_eq!(iter.next(), None);
303        assert_eq!(iter.next(), None);
304    }
305
306    #[test]
307    fn ref_iter() {
308        use crate::iter::JoinItem;
309
310        let data = vec!["Hello", "World"];
311        let join = data.join_with(' ');
312        let mut iter = (&join).into_iter();
313
314        assert_eq!(iter.next(), Some(JoinItem::Element(&"Hello")));
315        assert_eq!(iter.next(), Some(JoinItem::Separator(&' ')));
316        assert_eq!(iter.next(), Some(JoinItem::Element(&"World")));
317        assert_eq!(iter.next(), None);
318        assert_eq!(iter.next(), None);
319    }
320
321    #[cfg(feature = "token-stream")]
322    #[test]
323    fn to_tokens() {
324        use crate::separators::NoSeparator;
325        use quote::quote;
326
327        let functions = vec![
328            quote! {
329                fn test1() {}
330            },
331            quote! {
332                fn test2() {}
333            },
334        ];
335        let join = functions.join_with(NoSeparator);
336
337        let result = quote! { #join };
338        let target = quote! {
339            fn test1() {}
340            fn test2() {}
341        };
342
343        assert_eq!(result.to_string(), target.to_string());
344    }
345}