How can I spread a Vec<> into the arguments of format!()?

Viewed 671

I have a Vec of strings (str or String), and I would like to use them as the arguments for format!(). If the ... syntax of JS was available, I'd do something like this:

let data = vec!["A", "B", "C"];
let result = format!("{} says hello to {} but not to {}", ...data);

Is there any alternative in Rust that would make something like this possible, and ideally without it being incredibly verbose?

I assume part of the difficulty is that the Vec might not have the right number of arguments, so it would be acceptable to me for it to panic if it has the wrong number.

4 Answers

There is currently no way to do that without writing much code by hand or using procedural macro to generate necessary code.

As a more simple workaround you may try to use dynfmt, which provides way to pass arguments dynamically.

use dynfmt::{Format, SimpleCurlyFormat};

let formatted = SimpleCurlyFormat.format("{} says hello to {} but not to {}", &["A", "B", "C"]);
assert_eq!("A says hello to B but not to C", formatted.expect("formatting failed"));

The dyn-fmt crate looks like exactly what I need. It specifies a trait which adds a format() method to strings, which takes an Iterator. Any extra arguments are ignored, and missing ones are replaced with an empty string, so it won't panic. If you don't need format!()'s various formatting options, then it looks like a really good solid option.

use dyn_fmt::AsStrFormatExt;
let data = vec!["A", "B", "C"];
let result = "{} says hello to {} but not to {}".format(data);
assert_eq!(result, "A says hello to B but not to C");

You cannot expand them as you would do in js or python. But you can use join on a Vec<String> or Vec<&str>:

let data = vec!["A", "B", "C"];
let result = data.join("->");

Playground

As per nightly you could use intersperse_with and a combination of iterators:

    let data = vec!["A", "B", "C"];
    let phrases = vec![" says hello to ", " but not to "];
    let mut separators = phrases.iter().map(|x| x.to_string());
    let result = data
        .iter()
        .map(|x| x.to_string())
        .intersperse_with(|| separators.next().unwrap())
        .collect::<String>();

Playground

I thought producing a Vec<String> was wasteful in @Netwave's answer, so I improved it using iter.flat_map():

fn flat_map_niave(b: &mut Bencher) {
    let data = vec!["A", "B", "C"];
    let separators = vec![" says hello to ", " but not to "];
    b.iter(|| {
        let mut phrases = separators.iter();
        data.iter()
            .intersperse_with(|| phrases.next().unwrap())
            .flat_map(|s| s.chars())
            .collect::<String>()
    });
}

There are various tricks to improve performance at the expense of readability, I'll leave these in the playground.

  • flat_map_improved: Use a Vec<u8> and String::from_utf8()
  • flat_map_unchecked: Use a Vec<u8> and String::from_utf8_unchecked()
running 4 tests
test flat_map_improved  ... bench:         134 ns/iter (+/- 17)
test flat_map_niave     ... bench:         145 ns/iter (+/- 9)
test flat_map_unchecked ... bench:         116 ns/iter (+/- 6)
test vec_of_strings     ... bench:         235 ns/iter (+/- 6)
Related