Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to randomly select a format string

Sometimes there are many ways for a program to phrase a message containing a dynamic value to its users. For instance:

  • "{} minutes remaining."
  • "You need to finish in less than {} minutes."

Not all of the messages contain the value as a mere prefix or suffix. In a dynamic language, this would've seemed the logical task for string formatting.

For media where repetitiveness is undesirable (e.g. Slack channels) there are so many different phrasings that producing each final String to output using something like:

pub fn h(x: usize) -> String {
    rand::sample(rand::thread_rng(), vec![
        format!("{} minutes remain.", x),
        format!("Hurry up; only {} minutes left to finish.", x),
        format!("Haste advisable; time ends in {}.", x),
        /* (insert many more elements here) */
    ], 1).first().unwrap_or(format!("{}", x))
}

Would be:

  • Tedious to author, with respect to typing out format!(/*...*/, x) each time.
  • Wasteful of memory+clock-cycles as every single possibility is fully-generated before one is selected, discarding the others.

Is there any way to avoid these shortcomings?

Were it not for the compile-time evaluation of format strings, a function returning a randomly-selected &'static str (from a static slice) to pass into format!, would have been the preferred solution.

like image 666
mmirate Avatar asked Dec 21 '25 01:12

mmirate


2 Answers

Rust supports defining functions inside functions. We can build a slice of function pointers, have rand::sample pick one of them and then call the selected function.

extern crate rand;

use rand::Rng;

pub fn h(x: usize) -> String {
    fn f0(x: usize) -> String {
        format!("{} minutes remain.", x)
    }

    fn f1(x: usize) -> String {
        format!("Hurry up; only {} minutes left to finish.", x)
    }

    fn f2(x: usize) -> String {
        format!("Haste advisable; time ends in {}.", x)
    }

    let formats: &[fn(usize) -> String] = &[f0, f1, f2];
    (*rand::thread_rng().choose(formats).unwrap())(x)
}

This addresses the "wasteful" aspect of your original solution, but not the "tedious" aspect. We can reduce the amount of repetition by using a macro. Note that macros defined within a function are local to that function too! This macro is taking advantage from Rust's hygienic macros to define multiple functions named f, so that we don't need to supply a name for each function when using the macro.

extern crate rand;

use rand::Rng;

pub fn h(x: usize) -> String {
    macro_rules! messages {
        ($($fmtstr:tt,)*) => {
            &[$({
                fn f(x: usize) -> String {
                    format!($fmtstr, x)
                }
                f
            }),*]
        }
    }

    let formats: &[fn(usize) -> String] = messages!(
        "{} minutes remain.",
        "Hurry up; only {} minutes left to finish.",
        "Haste advisable; time ends in {}.",
    );
    (*rand::thread_rng().choose(formats).unwrap())(x)
}
like image 108
Francis Gagné Avatar answered Dec 24 '25 01:12

Francis Gagné


My suggestion would be to use a match to avoid unnecessary computation and keep the code as compact as possible:

use rand::{thread_rng, Rng};

let mut rng = thread_rng();
let code: u8 = rng.gen_range(0, 5);
let time = 5;
let response = match code {
    0 => format!("Running out of time! {} seconds left", time),
    1 => format!("Quick! {} seconds left", time),
    2 => format!("Hurry, there are {} seconds left", time),
    3 => format!("Faster! {} seconds left", time),
    4 => format!("Only {} seconds left", time),
    _ => unreachable!()
};

(Playground link)

Admittedly, it's a little bit ugly to match numbers literally, but it's probably the shortest you can get it.

like image 43
Aurora0001 Avatar answered Dec 24 '25 01:12

Aurora0001



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!