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}