comma_separated/
lib.rs

1//! Iterator over a comma-seperated string, ignoring any commas inside quotes
2//!
3//! # Example
4//!
5//! ```rust
6//! # use comma_separated::CommaSeparatedIterator;
7//! # fn main() {
8//! let input = r#"foo,"bar",'quoted, part'"#;
9//! let iterator = CommaSeparatedIterator::new(input);
10//! assert_eq!(vec!["foo", "\"bar\"", "'quoted, part'"], iterator.collect::<Vec<_>>());
11//! # }
12//! ```
13
14#[derive(Copy, Clone)]
15enum CommaSeparatedIteratorState {
16    /// Non quoted part
17    Default,
18    /// Inside a quote
19    Quoted(Quote),
20    /// After escape character inside quote
21    QuotedEscape(Quote),
22}
23
24#[derive(Copy, Clone)]
25enum Quote {
26    Single,
27    Double,
28}
29
30pub struct CommaSeparatedIterator<'a> {
31    remaining: &'a str,
32}
33
34impl<'a> CommaSeparatedIterator<'a> {
35    /// Create a new iterator, splitting the input into comma-seperated parts with handling of quoted segments
36    pub fn new(text: &'a str) -> Self {
37        Self { remaining: text }
38    }
39}
40
41impl<'a> Iterator for CommaSeparatedIterator<'a> {
42    type Item = &'a str;
43
44    fn next(&mut self) -> Option<Self::Item> {
45        if self.remaining.is_empty() {
46            return None;
47        }
48
49        let mut state = CommaSeparatedIteratorState::Default;
50        let char_indices = self.remaining.char_indices();
51
52        for (i, c) in char_indices {
53            state = match (state, c) {
54                (CommaSeparatedIteratorState::Default, '"') => {
55                    CommaSeparatedIteratorState::Quoted(Quote::Double)
56                }
57                (CommaSeparatedIteratorState::Default, '\'') => {
58                    CommaSeparatedIteratorState::Quoted(Quote::Single)
59                }
60                (CommaSeparatedIteratorState::Quoted(Quote::Double), '"')
61                | (CommaSeparatedIteratorState::Quoted(Quote::Single), '\'') => {
62                    CommaSeparatedIteratorState::Default
63                }
64                (CommaSeparatedIteratorState::Quoted(quote), '\\') => {
65                    CommaSeparatedIteratorState::QuotedEscape(quote)
66                }
67                (CommaSeparatedIteratorState::Quoted(quote), _) => {
68                    CommaSeparatedIteratorState::Quoted(quote)
69                }
70                (CommaSeparatedIteratorState::QuotedEscape(quote), _) => {
71                    CommaSeparatedIteratorState::Quoted(quote)
72                }
73                (CommaSeparatedIteratorState::Default, ',') => {
74                    let result = &self.remaining[0..i];
75                    self.remaining = &self.remaining[i + 1..];
76                    return Some(result);
77                }
78                (CommaSeparatedIteratorState::Default, _) => CommaSeparatedIteratorState::Default,
79            };
80        }
81        let result = self.remaining;
82        self.remaining = "";
83        Some(result)
84    }
85}
86
87impl<'a> DoubleEndedIterator for CommaSeparatedIterator<'a> {
88    fn next_back(&mut self) -> Option<Self::Item> {
89        if self.remaining.is_empty() {
90            return None;
91        }
92
93        let mut state = CommaSeparatedIteratorState::Default;
94        let mut char_indices = self.remaining.char_indices().rev().peekable();
95
96        while let Some((i, c)) = char_indices.next() {
97            state = match (state, c) {
98                (CommaSeparatedIteratorState::Default, '"') => {
99                    CommaSeparatedIteratorState::Quoted(Quote::Double)
100                }
101                (CommaSeparatedIteratorState::Default, '\'') => {
102                    CommaSeparatedIteratorState::Quoted(Quote::Single)
103                }
104                (CommaSeparatedIteratorState::Quoted(quote @ Quote::Double), '"')
105                | (CommaSeparatedIteratorState::Quoted(quote @ Quote::Single), '\'') => {
106                    if char_indices.peek().map(|(_, c)| *c) == Some('\\') {
107                        CommaSeparatedIteratorState::Quoted(quote)
108                    } else {
109                        CommaSeparatedIteratorState::Default
110                    }
111                }
112                (CommaSeparatedIteratorState::Quoted(quote), _) => {
113                    CommaSeparatedIteratorState::Quoted(quote)
114                }
115                (CommaSeparatedIteratorState::QuotedEscape(quote), _) => {
116                    CommaSeparatedIteratorState::Quoted(quote)
117                }
118                (CommaSeparatedIteratorState::Default, ',') => {
119                    let result = &self.remaining[i + 1..];
120                    self.remaining = &self.remaining[0..i];
121                    return Some(result);
122                }
123                (CommaSeparatedIteratorState::Default, _) => CommaSeparatedIteratorState::Default,
124            };
125        }
126
127        let result = self.remaining;
128        self.remaining = "";
129        Some(result)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::CommaSeparatedIterator;
136
137    #[test]
138    fn test_comma_separated_iterator() {
139        assert_eq!(
140            vec!["abc", "def", " ghi", "\tjkl", "mno", "\tpqr"],
141            CommaSeparatedIterator::new("abc,def, ghi,\tjkl,mno,\tpqr").collect::<Vec<&str>>()
142        );
143        assert_eq!(
144            vec!["\tpqr", "mno", "\tjkl", " ghi", "def", "abc"],
145            CommaSeparatedIterator::new("abc,def, ghi,\tjkl,mno,\tpqr")
146                .rev()
147                .collect::<Vec<&str>>()
148        );
149
150        assert_eq!(
151            vec![
152                r#""abc,def""#,
153                " \"ghi\"",
154                "\"jkl\" ",
155                " \"mno\"",
156                "pqr",
157                " \"abc, def\"",
158                " foo",
159                " \" foo\"",
160                " ',foo'",
161                " \"fo'o\"",
162            ],
163            CommaSeparatedIterator::new(
164                r#""abc,def", "ghi","jkl" , "mno",pqr, "abc, def", foo, " foo", ',foo', "fo'o""#
165            )
166            .collect::<Vec<&str>>()
167        );
168        assert_eq!(
169            vec![
170                " \"fo'o\"",
171                " ',foo'",
172                " \" foo\"",
173                " foo",
174                " \"abc, def\"",
175                "pqr",
176                " \"mno\"",
177                "\"jkl\" ",
178                " \"ghi\"",
179                r#""abc,def""#,
180            ],
181            CommaSeparatedIterator::new(
182                r#""abc,def", "ghi","jkl" , "mno",pqr, "abc, def", foo, " foo", ',foo', "fo'o""#
183            )
184            .rev()
185            .collect::<Vec<&str>>()
186        );
187
188        let mut iter = CommaSeparatedIterator::new("a,b,c,d");
189        assert_eq!(Some("a"), iter.next());
190        assert_eq!(Some("d"), iter.next_back());
191        assert_eq!(Some("b"), iter.next());
192        assert_eq!(Some("c"), iter.next_back());
193    }
194}