entroll_core/
ranges.rs

1use std::ops::RangeInclusive;
2
3/// Trait for types that can generate inclusive ranges and define a dash literal.
4pub trait Ranger<T> {
5    type Range;
6
7    fn dash() -> T;
8    fn range(start: T, end: T) -> Self::Range;
9}
10
11impl<I: Iterator<Item = char>> Ranger<char> for I {
12    type Range = RangeInclusive<char>;
13
14    fn dash() -> char {
15        '-'
16    }
17
18    fn range(start: char, end: char) -> RangeInclusive<char> {
19        start..=end
20    }
21}
22
23impl<I: Iterator<Item = u8>> Ranger<u8> for I {
24    type Range = RangeInclusive<u8>;
25
26    fn dash() -> u8 {
27        b'-'
28    }
29
30    fn range(start: u8, end: u8) -> RangeInclusive<u8> {
31        start..=end
32    }
33}
34
35/// An iterator that expands dash-separated ranges into their constituent items.
36///
37/// Wraps a `Peekable<I>` and produces items of `I::Item`, handling cases
38/// where a dash indicates an inclusive range.
39/// end (e.g., as a trailing dash), it is yielded as its own literal.
40/// # Examples
41///
42/// ```rust
43/// use entroll_core::Ranges;
44/// let expanded: Vec<char> = "a-cx-z".chars().ranges().collect();
45/// assert_eq!(expanded, "abcxyz".chars().collect::<Vec<_>>());
46///
47/// // Trailing dash is returned as a literal
48/// let trailing: Vec<char> = "a-".chars().ranges().collect();
49/// assert_eq!(trailing, vec!['a', '-']);
50/// ```
51pub struct Iter<I: Iterator, R: Ranger<I::Item>> {
52    iter: std::iter::Peekable<I>,
53    range: Option<R::Range>,
54}
55
56impl<I: Iterator, R> Iterator for Iter<I, R>
57where
58    I::Item: PartialEq + PartialOrd + Copy,
59    R: Ranger<I::Item>,
60    R::Range: Iterator<Item = I::Item>,
61{
62    type Item = I::Item;
63
64    fn next(&mut self) -> Option<Self::Item> {
65        if let Some(ref mut range) = self.range {
66            if let Some(next) = range.next() {
67                return Some(next);
68            }
69        };
70        if let Some(start) = self.iter.next() {
71            if self.iter.next_if_eq(&R::dash()).is_some() {
72                if let Some(end) = self.iter.next() {
73                    self.range = Some(R::range(start, end));
74                    self.next()
75                } else {
76                    // case like "a-"
77                    // '-' has already been consumed
78                    // so we need to put it back
79                    self.range = Some(R::range(R::dash(), R::dash()));
80                    Some(start)
81                }
82            } else {
83                Some(start)
84            }
85        } else {
86            None
87        }
88    }
89}
90
91/// Extension trait to provide `.ranges()` on iterators of `char` or `u8`.
92///
93/// Calling `.ranges()` on such an iterator will return an `Iter` that expands
94/// dash-separated ranges.
95///
96/// # Examples
97/// ```rust
98/// use entroll_core::Ranges;
99/// let expanded: Vec<char> = "a-cx-z".chars().ranges().collect();
100/// assert_eq!(expanded, "abcxyz".chars().collect::<Vec<_>>());
101/// ```
102pub trait Ranges {
103    type Item;
104
105    fn ranges(self) -> Iter<impl Iterator<Item = Self::Item>, impl Ranger<Self::Item>>
106    where
107        Self: Sized;
108}
109
110impl<I: Iterator + Ranger<I::Item>> Ranges for I {
111    type Item = I::Item;
112
113    /// Wraps the iterator to expand dash-separated ranges.
114    #[allow(refining_impl_trait)]
115    fn ranges(self) -> Iter<impl Iterator<Item = Self::Item>, I>
116    where
117        Self: Sized,
118    {
119        Iter {
120            iter: self.peekable(),
121            range: None,
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    #[allow(clippy::unnecessary_to_owned)]
132    fn test_str() {
133        fn ranges<S: AsRef<str>>(s: S) -> Vec<char> {
134            s.as_ref().chars().ranges().collect()
135        }
136
137        let mut iter = "a-c".chars().ranges();
138        assert_eq!(iter.next(), Some('a'));
139        assert_eq!(iter.next(), Some('b'));
140        assert_eq!(iter.next(), Some('c'));
141        assert_eq!(iter.next(), None);
142
143        assert_eq!(ranges(""), vec![]);
144        assert_eq!(ranges("a"), vec!['a']);
145        assert_eq!(ranges("-"), vec!['-']);
146        assert_eq!(ranges("a-"), vec!['a', '-']);
147        assert_eq!(ranges("a-df"), vec!['a', 'b', 'c', 'd', 'f']);
148        assert_eq!(ranges("-a-c"), vec!['-', 'a', 'b', 'c']);
149        assert_eq!(ranges("a-c-"), vec!['a', 'b', 'c', '-']);
150        assert_eq!(ranges("ga-df"), vec!['g', 'a', 'b', 'c', 'd', 'f']);
151        assert_eq!(ranges("a-f0-9"), ranges("abcdef0123456789"));
152        assert_eq!(ranges("!-/"), ranges("!\"#$%&'()*+,-./".to_string()));
153    }
154
155    #[test]
156    fn test_u8() {
157        fn ranges(s: &[u8]) -> Vec<u8> {
158            s.iter().copied().ranges().collect()
159        }
160
161        let mut iter = b"a-c".iter().copied().ranges();
162        assert_eq!(iter.next(), Some(b'a'));
163        assert_eq!(iter.next(), Some(b'b'));
164        assert_eq!(iter.next(), Some(b'c'));
165        assert_eq!(iter.next(), None);
166
167        assert_eq!(ranges(b""), vec![]);
168        assert_eq!(ranges(b"a"), vec![b'a']);
169        assert_eq!(ranges(b"-"), vec![b'-']);
170        assert_eq!(ranges(b"a-"), vec![b'a', b'-']);
171        assert_eq!(ranges(b"a-df"), vec![b'a', b'b', b'c', b'd', b'f']);
172        assert_eq!(ranges(b"-a-c"), vec![b'-', b'a', b'b', b'c']);
173        assert_eq!(ranges(b"a-c-"), vec![b'a', b'b', b'c', b'-']);
174        assert_eq!(ranges(b"ga-df"), vec![b'g', b'a', b'b', b'c', b'd', b'f']);
175        assert_eq!(ranges(b"a-f0-9"), ranges(b"abcdef0123456789"));
176        assert_eq!(ranges(b"!-/"), ranges(b"!\"#$%&'()*+,-./"));
177    }
178}