Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
297 lines (261 sloc) 8.92 KB
//! Core join type and related traits
use core::fmt::{self, Display, Formatter};
use crate::iter::{JoinItem, JoinIter, JoinableIterator};
use crate::separators::NoSeparator;
/// A trait for converting collections into [`Join`] instances.
///
/// This trait is implemented for all referentially iterable types; that is,
/// for all types for which `&T: IntoIterator`. See [`join_with`][Joinable::join_with]
/// for an example of its usage.
pub trait Joinable: Sized {
/// Combine this object with a separator to create a new [`Join`] instance.
/// Note that the separator does not have to share the same type as the
/// iterator's values.
///
/// # Examples
///
/// ```
/// use joinery::Joinable;
///
/// let parts = vec!["this", "is", "a", "sentence"];
/// let join = parts.join_with(' ');
/// assert_eq!(join.to_string(), "this is a sentence");
/// ```
fn join_with<S>(self, sep: S) -> Join<Self, S>;
/// Join this object with an [empty separator](NoSeparator). When rendered
/// with [`Display`], the underlying elements will be directly concatenated.
/// Note that the separator, while empty, is still present, and will show
/// up if you iterate this instance.
///
/// # Examples
///
/// ```
/// use joinery::Joinable;
///
/// let parts = vec!['a', 'b', 'c', 'd', 'e'];
/// let join = parts.join_concat();
/// assert_eq!(join.to_string(), "abcde");
/// ```
fn join_concat(self) -> Join<Self, NoSeparator> {
self.join_with(NoSeparator)
}
}
impl<T> Joinable for T
where
for<'a> &'a T: IntoIterator,
{
fn join_with<S>(self, sep: S) -> Join<Self, S> {
Join {
collection: self,
sep,
}
}
}
/// A trait for using a separator to produce a [`Join`].
///
/// This trait provides a more python-style interface for performing joins.
/// Rather use [`collection.join_with`][Joinable::join_with], you can instead
/// use:
///
/// ```
/// use joinery::Separator;
///
/// let join = ", ".separate([1, 2, 3, 4]);
/// assert_eq!(join.to_string(), "1, 2, 3, 4");
/// ```
///
/// By default, [`Separator`] is implemented for [`char`] and [`&str`][str], as
/// well as all the separator types in [separator][separator@].
///
/// Note that any type can be used as a separator in a [`Join`] when
/// creating one via [`Joinable::join_with`]. The [`Separator`] trait and its
/// implementations on [`char`] and [`&str`][str] are provided simply as
/// a convenience.
pub trait Separator: Sized {
fn separate<T: Joinable>(self, collection: T) -> Join<T, Self> {
collection.join_with(self)
}
}
impl Separator for char {}
impl<'a> Separator for &'a str {}
/// The primary data structure for representing a joined sequence.
///
/// It contains a collection and a separator, and represents the elements of the
/// collection with the separator dividing each element. A collection is defined
/// as any type for which `&T: IntoIterator`; that is, any time for which references
/// to the type are iterable.
///
/// A [`Join`] is created with [`Joinable::join_with`], [`Separator::separate`], or
/// [`JoinableIterator::join_with`]. Its main use is an implementation of [`Display`],
/// which writes out the elements of the underlying collection, separated by the
/// separator. It also implements [`IntoIterator`], using a [`JoinIter`].
///
/// # Examples
///
/// Writing via [`Display`]:
///
/// ```
/// use joinery::Joinable;
/// use std::fmt::Write;
///
/// let content = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/// let join = content.join_with(", ");
///
/// let mut buffer = String::new();
/// write!(buffer, "Numbers: {}", join);
///
/// assert_eq!(buffer, "Numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9");
///
/// // Don't forget that `Display` gives to `ToString` for free!
/// assert_eq!(join.to_string(), "1, 2, 3, 4, 5, 6, 7, 8, 9")
/// ```
///
/// Iterating via [`IntoIterator`]:
///
/// ```
/// use joinery::{Separator, JoinItem};
///
/// let content = [0, 1, 2];
/// let join = ", ".separate(content);
/// let mut join_iter = join.into_iter();
///
/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&0)));
/// assert_eq!(join_iter.next(), Some(JoinItem::Separator(&", ")));
/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&1)));
/// assert_eq!(join_iter.next(), Some(JoinItem::Separator(&", ")));
/// assert_eq!(join_iter.next(), Some(JoinItem::Element(&2)));
/// assert_eq!(join_iter.next(), None);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct Join<C, S> {
collection: C,
sep: S,
}
impl<C, S> Join<C, S> {
/// Get a reference to the separator.
pub fn sep(&self) -> &S {
&self.sep
}
/// Get a reference to the underlying collection.
pub fn collection(&self) -> &C {
&self.collection
}
/// Consume the join, and return the underlying collection.
pub fn into_collection(self) -> C {
self.collection
}
/// Consume `self` and return underlying collection and separator.
pub fn into_parts(self) -> (C, S) {
(self.collection, self.sep)
}
}
mod private {
use core::fmt;
/// This variant Display trait includes a lifetime bound, which is (for
/// some reason) required in order to make the trait bounds work when implementing
/// [`Display`] for [`Join`]. See https://stackoverflow.com/q/53364798/864406
/// for details.
pub trait Display<'a>: fmt::Display {}
impl<'a, T: fmt::Display> Display<'a> for T {}
}
impl<C, S: Display> Display for Join<C, S>
where
for<'a> &'a C: IntoIterator,
for<'a> <&'a C as IntoIterator>::Item: private::Display<'a>,
{
/// Format the joined collection, by writing out each element of the
/// collection, separated by the separator.
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let mut iter = self.collection.into_iter();
match iter.next() {
None => Ok(()),
Some(first) => {
first.fmt(f)?;
iter.try_for_each(move |element| {
self.sep.fmt(f)?;
element.fmt(f)
})
}
}
}
}
impl<C: IntoIterator, S: Clone> IntoIterator for Join<C, S> {
type IntoIter = JoinIter<C::IntoIter, S>;
type Item = JoinItem<C::Item, S>;
/// Create a consuming iterator from a `Join`. This iterator consumes the
/// elements of the underlying collection, and intersperses them with clones
/// of the separator. See the [`JoinIter`] documentation for more details.
fn into_iter(self) -> Self::IntoIter {
self.collection.into_iter().iter_join_with(self.sep)
}
}
impl<'a, C, S> IntoIterator for &'a Join<C, S>
where
&'a C: IntoIterator,
{
type IntoIter = JoinIter<<&'a C as IntoIterator>::IntoIter, &'a S>;
type Item = JoinItem<<&'a C as IntoIterator>::Item, &'a S>;
/// Create a referential iterator over the join. This iterator iterates
/// over references to the underlying collection, interspersed with references
/// to the separator. See the [`JoinIter`] documentation for more details.
fn into_iter(self) -> Self::IntoIter {
self.collection.into_iter().iter_join_with(&self.sep)
}
}
#[cfg(test)]
mod tests {
use super::{Joinable, Separator};
#[test]
fn empty() {
let data: Vec<String> = Vec::new();
let join = data.join_with(", ");
let result = join.to_string();
assert_eq!(result, "");
}
#[test]
fn single() {
let data = vec!["Hello"];
let join = data.join_with(", ");
let result = join.to_string();
assert_eq!(result, "Hello");
}
#[test]
fn simple_join() {
let data = vec!["This", "is", "a", "sentence"];
let join = data.join_with(' ');
let result = join.to_string();
assert_eq!(result, "This is a sentence");
}
#[test]
fn join_via_separator() {
let data = vec!["This", "is", "a", "sentence"];
let join = ' '.separate(data);
let result = join.to_string();
assert_eq!(result, "This is a sentence");
}
#[test]
fn iter() {
use crate::iter::JoinItem;
let data = vec!["Hello", "World"];
let join = data.join_with(' ');
let mut iter = join.into_iter();
assert_eq!(iter.next(), Some(JoinItem::Element("Hello")));
assert_eq!(iter.next(), Some(JoinItem::Separator(' ')));
assert_eq!(iter.next(), Some(JoinItem::Element("World")));
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
#[test]
fn ref_iter() {
use crate::iter::JoinItem;
let data = vec!["Hello", "World"];
let join = data.join_with(' ');
let mut iter = (&join).into_iter();
assert_eq!(iter.next(), Some(JoinItem::Element(&"Hello")));
assert_eq!(iter.next(), Some(JoinItem::Separator(&' ')));
assert_eq!(iter.next(), Some(JoinItem::Element(&"World")));
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None);
}
}