entroll-core 0.1.2

Generate random passwords and print entropy in bits.
Documentation
use std::ops::RangeInclusive;

/// Trait for types that can generate inclusive ranges and define a dash literal.
pub trait Ranger<T> {
    type Range;

    fn dash() -> T;
    fn range(start: T, end: T) -> Self::Range;
}

impl<I: Iterator<Item = char>> Ranger<char> for I {
    type Range = RangeInclusive<char>;

    fn dash() -> char {
        '-'
    }

    fn range(start: char, end: char) -> RangeInclusive<char> {
        start..=end
    }
}

impl<I: Iterator<Item = u8>> Ranger<u8> for I {
    type Range = RangeInclusive<u8>;

    fn dash() -> u8 {
        b'-'
    }

    fn range(start: u8, end: u8) -> RangeInclusive<u8> {
        start..=end
    }
}

/// An iterator that expands dash-separated ranges into their constituent items.
///
/// Wraps a `Peekable<I>` and produces items of `I::Item`, handling cases
/// where a dash indicates an inclusive range.
/// end (e.g., as a trailing dash), it is yielded as its own literal.
/// # Examples
///
/// ```rust
/// use entroll_core::Ranges;
/// let expanded: Vec<char> = "a-cx-z".chars().ranges().collect();
/// assert_eq!(expanded, "abcxyz".chars().collect::<Vec<_>>());
///
/// // Trailing dash is returned as a literal
/// let trailing: Vec<char> = "a-".chars().ranges().collect();
/// assert_eq!(trailing, vec!['a', '-']);
/// ```
pub struct Iter<I: Iterator, R: Ranger<I::Item>> {
    iter: std::iter::Peekable<I>,
    range: Option<R::Range>,
}

impl<I: Iterator, R> Iterator for Iter<I, R>
where
    I::Item: PartialEq + PartialOrd + Copy,
    R: Ranger<I::Item>,
    R::Range: Iterator<Item = I::Item>,
{
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(ref mut range) = self.range {
            if let Some(next) = range.next() {
                return Some(next);
            }
        };
        if let Some(start) = self.iter.next() {
            if self.iter.next_if_eq(&R::dash()).is_some() {
                if let Some(end) = self.iter.next() {
                    self.range = Some(R::range(start, end));
                    self.next()
                } else {
                    // case like "a-"
                    // '-' has already been consumed
                    // so we need to put it back
                    self.range = Some(R::range(R::dash(), R::dash()));
                    Some(start)
                }
            } else {
                Some(start)
            }
        } else {
            None
        }
    }
}

/// Extension trait to provide `.ranges()` on iterators of `char` or `u8`.
///
/// Calling `.ranges()` on such an iterator will return an `Iter` that expands
/// dash-separated ranges.
///
/// # Examples
/// ```rust
/// use entroll_core::Ranges;
/// let expanded: Vec<char> = "a-cx-z".chars().ranges().collect();
/// assert_eq!(expanded, "abcxyz".chars().collect::<Vec<_>>());
/// ```
pub trait Ranges {
    type Item;

    fn ranges(self) -> Iter<impl Iterator<Item = Self::Item>, impl Ranger<Self::Item>>
    where
        Self: Sized;
}

impl<I: Iterator + Ranger<I::Item>> Ranges for I {
    type Item = I::Item;

    /// Wraps the iterator to expand dash-separated ranges.
    #[allow(refining_impl_trait)]
    fn ranges(self) -> Iter<impl Iterator<Item = Self::Item>, I>
    where
        Self: Sized,
    {
        Iter {
            iter: self.peekable(),
            range: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(clippy::unnecessary_to_owned)]
    fn test_str() {
        fn ranges<S: AsRef<str>>(s: S) -> Vec<char> {
            s.as_ref().chars().ranges().collect()
        }

        let mut iter = "a-c".chars().ranges();
        assert_eq!(iter.next(), Some('a'));
        assert_eq!(iter.next(), Some('b'));
        assert_eq!(iter.next(), Some('c'));
        assert_eq!(iter.next(), None);

        assert_eq!(ranges(""), vec![]);
        assert_eq!(ranges("a"), vec!['a']);
        assert_eq!(ranges("-"), vec!['-']);
        assert_eq!(ranges("a-"), vec!['a', '-']);
        assert_eq!(ranges("a-df"), vec!['a', 'b', 'c', 'd', 'f']);
        assert_eq!(ranges("-a-c"), vec!['-', 'a', 'b', 'c']);
        assert_eq!(ranges("a-c-"), vec!['a', 'b', 'c', '-']);
        assert_eq!(ranges("ga-df"), vec!['g', 'a', 'b', 'c', 'd', 'f']);
        assert_eq!(ranges("a-f0-9"), ranges("abcdef0123456789"));
        assert_eq!(ranges("!-/"), ranges("!\"#$%&'()*+,-./".to_string()));
    }

    #[test]
    fn test_u8() {
        fn ranges(s: &[u8]) -> Vec<u8> {
            s.iter().copied().ranges().collect()
        }

        let mut iter = b"a-c".iter().copied().ranges();
        assert_eq!(iter.next(), Some(b'a'));
        assert_eq!(iter.next(), Some(b'b'));
        assert_eq!(iter.next(), Some(b'c'));
        assert_eq!(iter.next(), None);

        assert_eq!(ranges(b""), vec![]);
        assert_eq!(ranges(b"a"), vec![b'a']);
        assert_eq!(ranges(b"-"), vec![b'-']);
        assert_eq!(ranges(b"a-"), vec![b'a', b'-']);
        assert_eq!(ranges(b"a-df"), vec![b'a', b'b', b'c', b'd', b'f']);
        assert_eq!(ranges(b"-a-c"), vec![b'-', b'a', b'b', b'c']);
        assert_eq!(ranges(b"a-c-"), vec![b'a', b'b', b'c', b'-']);
        assert_eq!(ranges(b"ga-df"), vec![b'g', b'a', b'b', b'c', b'd', b'f']);
        assert_eq!(ranges(b"a-f0-9"), ranges(b"abcdef0123456789"));
        assert_eq!(ranges(b"!-/"), ranges(b"!\"#$%&'()*+,-./"));
    }
}