57

How do I convert an Iterator<&str> to a String, interspersed with a constant string such as "\n"? For instance, given:

let xs = vec!["first", "second", "third"];
let it = xs.iter();

One may produce a string s by collecting into a Vec<&str> and joining the result:

let s = it
    .map(|&x| x)
    .collect::<Vec<&str>>()
    .join("\n");

However, this unnecessarily allocates memory for a Vec<&str>.

Is there a more direct method?

10
  • 1
    Apologies - my original answer removed the iterator but your question is asking how to join an iterator and not allocate the extra vector. Commented May 8, 2019 at 4:41
  • 1
    Looks like the itertools crate doesn't allocate the vector Commented May 8, 2019 at 5:09
  • 2
    Note that depending on the exact characteristics of your iterator, collecting into a vector of slices and then joining could actually be faster than using Websterix's method or itertools, since SliceConcatExt::join can calculate the needed size for the full string ahead of time and thus definitely doesn't need to reallocate during accumulation; whereas the other methods may have to reallocate the string. You should definitely benchmark. Commented May 8, 2019 at 6:04
  • 1
    @chpio It has to allocate, but not reallocate if the iterator gives a good size hint. Commented May 8, 2019 at 12:45
  • 2
    How is this a duplicate?! Commented Jul 5, 2022 at 5:14

5 Answers 5

34

You could use the itertools crate for that. I use the intersperse helper in the example, it is pretty much the join equivalent for iterators.

cloned() is needed to convert &&str items to &str items, it is not doing any allocations. It can be eventually replaced by copied() when [email protected] gets a stable release.

use itertools::Itertools; // 0.8.0

fn main() {
    let words = ["alpha", "beta", "gamma"];
    let merged: String = words.iter().cloned().intersperse(", ").collect();
    assert_eq!(merged, "alpha, beta, gamma");
}

Playground

24

You can do it by using fold function of the iterator easily:

let s = it.fold(String::new(), |a, b| a + b + "\n");

The Full Code will be like following:

fn main() {
    let xs = vec!["first", "second", "third"];
    let it = xs.into_iter();

    // let s = it.collect::<Vec<&str>>().join("\n");

    let s = it.fold(String::new(), |a, b| a + b + "\n");
    let s = s.trim_end();

    println!("{:?}", s);
}

Playground

EDIT: After the comment of Sebastian Redl I have checked the performance cost of the fold usage and created a benchmark test on playground.

You can see that fold usage takes significantly more time for the many iterative approaches.

Did not check the allocated memory usage though.

9
  • 20
    The reason this is slow is because you're using + to create two new Strings on every iteration. If you use a single string (playground) it can work better than collect and join (playground).
    – mdonoughe
    Commented Dec 21, 2020 at 7:15
  • added black_box and create the vec for each test individually (because of cache warming) (playgroud). Playground isn't that good for benchmarking due to massive variance in latency/duration, but the fold variant seems to be slightly slower (over multiple runs).
    – chpio
    Commented Apr 19, 2021 at 12:23
  • 1
    v2 with black_box(xs).iter().copied() takes now twice as long for collect+join over fold (the black_box(xs) doesn't matter, xs is the same). <3 for microbenchmarking
    – chpio
    Commented Apr 19, 2021 at 12:54
  • 1
    Yes, they do
    – chpio
    Commented Apr 19, 2021 at 13:14
  • 1
    Another solution is let mut it = xs.into_iter(); let first = it.next().unwrap_or("").to_owned(); let r = it.fold(first, |a, b| a + "\n" + b); Then you end up with a String instead of &str
    – d2weber
    Commented Mar 16, 2023 at 20:43
12

With itertools, you have not only intersperse() but also join():

use itertools::Itertools;

let s = it.join("\n");

It is more general than intersperse() (it accepts any Display-implementing type) but therefore may be slower (I didn't benchmark though).

0

use Iterator::reduce.

fn main() {
    let it = ["1", "2", "3"].into_iter();
    let res = it.map(String::from).reduce(|acc, s| format!("{acc}, {s}")).unwrap_or_default();
    assert_eq!(&res, "1, 2, 3");
}

You can use Cow to avoid unnecessary allocation.

use std::borrow::Cow;

fn main() {
    let it = ["1", "2", "3"].into_iter();
    let res = it.map(Cow::from).reduce(|mut acc, s| {
        acc.to_mut().push('\n');
        acc.to_mut().push_str(&s);
        acc
    }).unwrap_or_default();
    assert_eq!(&res, "1\n2\n3");
}

-4

there's relevant example in rust documentation: here.

let words = ["alpha", "beta", "gamma"];

// chars() returns an iterator
let merged: String = words.iter()
                          .flat_map(|s| s.chars())
                          .collect();
assert_eq!(merged, "alphabetagamma");

You can also use Extend trait:

fn f<'a, I: Iterator<Item=&'a str>>(data: I) -> String {
    let mut ret = String::new();
    ret.extend(data);
    ret
}
2
  • 7
    This answer does not reproducing the OP's needs. OP is asking about interspersed with some constant string (e.g. "\n")?. Commented May 8, 2019 at 5:29
  • 1
    also this should work without flat_map, as String already implements Extend<&str>.
    – chpio
    Commented May 8, 2019 at 9:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.