string_join/
lib.rs

1//! String join
2//!
3//! Uses a python-like syntax to join any iterator-like thing with any string-like thing
4//!
5//! # Example
6//!
7//! ```rust
8//! use string_join::Join;
9//!
10//! assert_eq!("=".join(&["a", "b", "c"]), String::from("a=b=c"));
11//! ```
12//!
13//! You can also write a joined collection directly to an `io::Write`r
14//!
15//! ```rust,no_run
16//! use std::fs::File;
17//! use string_join::display::Join;
18//!
19//! # fn main() -> std::io::Result<()> {
20//! let mut f = File::create("foo.txt")?;
21//! "\n".write_join(&mut f, &['a', 'b', 'c'])?; // => writes `a\nb\nc` to the writer and returns
22//!                                             //    the number of bytes written (5, in this case)
23//! #   Ok(())
24//! # }
25//! ```
26//!
27//! # `AsRef<str>` vs `Display`
28//!
29//! There are two ways to convert items in a collection to a joined string. If all the items, as
30//! well as the separator, implement `AsRef<str>`, then we only have to allocate a single string
31//! and can write bytes into it. However, this limits the types that we can join. If we instead
32//! limit the types we can join to types that implement `Display`, we can join a lot more types,
33//! with the tradeoff that a lot more allocations will be happening.
34//!
35//! Therefore, there are two modules in this crate, `string_join::as_ref` and
36//! `string_join::display`. They both provide a trait called `Join` and both the traits have
37//! methods `write_join` and `join`. However, one is implemented for `AsRef<str>` types and one for
38//! `Display` types. This way, the user of the crate can decide which implementation they want to
39//! use.
40//!
41//! To keep backwards compatibility, `string_join::Join` is the same as
42//! `string_join::as_ref::Join`.
43
44pub use crate::as_ref::Join;
45
46/// A `Join` implementation for iterator item types that implement `Display`
47pub mod display {
48    /// This trait brings the `join` and `write_join` methods into scope.
49    pub trait Join {
50
51        /// Called on the separator, this takes a writer and something that can be turned into an
52        /// iterator and writes the joined result to the writer.
53        fn write_join<I: Display, W: Write, T: IntoIterator<Item=I>>(&self, writer: W, coll: T) -> io::Result<usize>;
54
55        /// Called on the separator, this takes something that can be turned into an iterator and
56        /// produces a heap-allocated string
57        fn join<I: Display, T: IntoIterator<Item=I>>(&self, coll: T) -> String {
58            let mut s = Vec::new();
59            // Safety: will only panic if we OOM, in which case we're screwed anyway
60            self.write_join(&mut s, coll).expect("This shouldn't fail");
61            // Safety: all inputs to write_join are `AsRef<str>` so they will all be valid utf-8 and
62            // therefore this can't fail
63            String::from_utf8(s).unwrap()
64        }
65    }
66
67    impl<A: AsRef<str>> Join for A {
68        fn write_join<I: Display, W: Write, T: IntoIterator<Item=I>>(&self, mut writer: W, coll: T) -> io::Result<usize> {
69            let mut iter = coll.into_iter();
70            let mut written = 0;
71            if let Some(first) = iter.next() {
72                let first = first.to_string();
73                let bytes = first.as_bytes();
74                writer.write_all(bytes)?;
75                written += bytes.len();
76            }
77            for s in iter {
78                let t1 = self.as_ref().as_bytes();
79                writer.write_all(t1)?;
80                written += t1.len();
81
82                let s = s.to_string();
83                let t2 = s.as_bytes();
84                writer.write_all(t2)?;
85                written += t2.len();
86            }
87            writer.flush()?;
88            io::Result::Ok(written)
89        }
90    }
91
92    #[cfg(test)]
93    mod tests {
94        #[test]
95        fn write_join_to_file() {
96            let v = vec![];
97            let mut cur = Cursor::new(v);
98            let written = "=".write_join(&mut cur, &['a', 'b', 'c']).expect("Couldn't write joined string");
99            let result = cur.into_inner();
100            assert_eq!(written, 5);
101            assert_eq!(&result[..], "a=b=c".as_bytes());
102        }
103
104        #[test]
105        fn join_with_heap_string() {
106            let equal = String::from("=");
107            let result = equal.join(&['a', 'b', 'c']);
108            assert_eq!("a=b=c", &result[..]);
109        }
110
111        #[test]
112        fn join_with_borrowed_string() {
113            let equal = String::from("=");
114            let eq = &equal[..];
115            let result = eq.join(&['a', 'b', 'c']);
116            assert_eq!("a=b=c", &result[..]);
117        }
118
119        #[test]
120        fn join_with_cow_string() {
121            let equal = String::from("=");
122            let eq: Cow<'_, str> = Cow::Borrowed(&equal[..]);
123            let result = eq.join(&['a', 'b', 'c']);
124            assert_eq!("a=b=c", &result[..]);
125        }
126
127        #[test]
128        fn join_hashset() {
129            let set = {
130                let mut set = HashSet::new();
131                set.insert('a');
132                set.insert('b');
133                set.insert('c');
134                set
135            };
136            let result = "=".join(&set);
137            assert!(vec!["a=b=c", "a=c=b", "b=a=c", "b=c=a", "c=a=b", "c=b=a"].contains(&&result[..]));
138        }
139
140        use std::io::Cursor;
141        use std::borrow::Cow;
142        use std::collections::HashSet;
143        use super::Join;
144    }
145
146    use std::{
147        fmt::Display,
148        io::{self, Write},
149    };
150}
151
152/// A `Join` implementation for iterator item types that implement `AsRef<str>`
153pub mod as_ref {
154
155    /// This trait brings the `join` and `write_join` methods into scope.
156    pub trait Join {
157
158        /// Called on the separator, this takes a writer and something that can be turned into an
159        /// iterator and writes the joined result to the writer.
160        fn write_join<I: AsRef<str>, W: Write, T: IntoIterator<Item=I>>(&self, writer: W, coll: T) -> io::Result<usize>;
161
162        /// Called on the separator, this takes something that can be turned into an iterator and
163        /// produces a heap-allocated string
164        fn join<I: AsRef<str>, T: IntoIterator<Item=I>>(&self, coll: T) -> String {
165            let mut s = Vec::new();
166            // Safety: will only panic if we OOM, in which case we're screwed anyway
167            self.write_join(&mut s, coll).expect("This shouldn't fail");
168            // Safety: all inputs to write_join are `AsRef<str>` so they will all be valid utf-8 and
169            // therefore this can't fail
170            String::from_utf8(s).unwrap()
171        }
172    }
173
174    impl<A: AsRef<str>> Join for A {
175        fn write_join<I: AsRef<str>, W: Write, T: IntoIterator<Item=I>>(&self, mut writer: W, coll: T) -> io::Result<usize> {
176            let mut iter = coll.into_iter();
177            let mut written = 0;
178            if let Some(first) = iter.next() {
179                let bytes = first.as_ref().as_bytes();
180                writer.write_all(bytes)?;
181                written += bytes.len();
182            }
183            for s in iter {
184                let t1 = self.as_ref().as_bytes();
185                writer.write_all(t1)?;
186                written += t1.len();
187
188                let t2 = s.as_ref().as_bytes();
189                writer.write_all(t2)?;
190                written += t2.len();
191            }
192            writer.flush()?;
193            io::Result::Ok(written)
194        }
195    }
196
197    #[cfg(test)]
198    mod tests {
199        #[test]
200        fn write_join_to_file() {
201            let v = vec![];
202            let mut cur = Cursor::new(v);
203            let written = "=".write_join(&mut cur, &["a", "b", "c"]).expect("Couldn't write joined string");
204            let result = cur.into_inner();
205            assert_eq!(written, 5);
206            assert_eq!(&result[..], "a=b=c".as_bytes());
207        }
208
209        #[test]
210        fn join_with_heap_string() {
211            let equal = String::from("=");
212            let result = equal.join(&["a", "b", "c"]);
213            assert_eq!("a=b=c", &result[..]);
214        }
215
216        #[test]
217        fn join_with_borrowed_string() {
218            let equal = String::from("=");
219            let eq = &equal[..];
220            let result = eq.join(&["a", "b", "c"]);
221            assert_eq!("a=b=c", &result[..]);
222        }
223
224        #[test]
225        fn join_with_cow_string() {
226            let equal = String::from("=");
227            let eq: Cow<'_, str> = Cow::Borrowed(&equal[..]);
228            let result = eq.join(&["a", "b", "c"]);
229            assert_eq!("a=b=c", &result[..]);
230        }
231
232        #[test]
233        fn join_hashset() {
234            let set = {
235                let mut set = HashSet::new();
236                set.insert("a");
237                set.insert("b");
238                set.insert("c");
239                set
240            };
241            let result = "=".join(&set);
242            assert!(vec!["a=b=c", "a=c=b", "b=a=c", "b=c=a", "c=a=b", "c=b=a"].contains(&&result[..]));
243        }
244
245        use std::io::Cursor;
246        use std::borrow::Cow;
247        use std::collections::HashSet;
248        use super::Join;
249    }
250
251    use std::{
252        io::{self, Write},
253    };
254}