Rust rand crate has a distr module for distributions, both trait and impls that simplify random generation, like Alphabetic, Alphanumeric.

It also includes a SampleString trait that gives us a sample_string fn on it's impl, for generating a sampling string of len n. So, if we want a random alphanumeric string we can just write:

1use rand::distr::{Alphanumeric, SampleString};
2
3fn main() {
4	println!("{}", Alphanumeric.sample_string(&mut rand::rng(), 20));
5}

This is incredibly useful, but distr doesn't include a Numeric distribution; which you might say isn't needed, since you can:

  • Just generate a random number. Why store a number as a string. How frequently do you need a number that can have leading zeros? And why would you want a number like that? They'll probably get cut somewhere, which will only cause a headache. Then here you go, here is a simple random number generation from a given range:
    1use rand::Rng;
    2
    3fn random_num<R: Rng>(rng: &mut R, len: u32) -> u64 {
    4	return rng.random_range(10u64.pow(len - 1)..10u64.pow(len));
    5}
    
  • Very simply do what SampleString would do for you in that situation:
     1use rand::Rng;
     2
     3const DIGITS: &[u8] = b"0123456789";
     4
     5fn random_num<R: Rng>(rng: &mut R, len: usize) -> String {
     6	return String::from_utf8(
     7		(0..len)
     8		.map(|_| DIGITS[rng.random_range(0..DIGITS.len())])
     9		.collect()
    10	).unwrap();
    11}
    

But why would we want to do things in a reasonable way. Or maybe, we want to generate a random string from a given distribution, given conditionally in a generic way, like so:

use rand::{distr::{Alphabetic, Alphanumeric, Distribution, SampleString}, Rng};

fn rand_str<R, D>(rng: &mut R, distr: D, len: usize) -> String
where R: Rng, D: Distribution<u8> + SampleString {
	return distr.sample_string(rng, len);
}

fn main() {
    let mut rng = rand::rng();
    for i in 0..5 {
        if i % 2 == 0 {
            println!("A: {}", rand_str(&mut rng, Alphanumeric, 10));
        } else {
            println!("B: {}", rand_str(&mut rng, Alphabetic, 10));
        }
    }
}

This is getting quite complex, but maybe we need that. Then, we need a Numeric distribution of our own.

Implemented based on how rand does it for other distributions with a test, here it is below:

 1use rand::{distr::{Distribution, SampleString}, Rng};
 2
 3#[derive(Copy, Clone, Debug, Default)]
 4pub struct Numeric;
 5
 6impl Numeric {
 7	const CHARSET: &[u8] = b"0123456789";
 8	const RANGE: usize = Numeric::CHARSET.len();
 9}
10
11impl Distribution<u8> for Numeric {
12    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> u8 {
13        return Numeric::CHARSET[rng.random_range(0..Numeric::RANGE)];
14    }
15}
16
17impl SampleString for Numeric {
18    fn append_string<R: Rng + ?Sized>(&self, rng: &mut R, string: &mut String, len: usize) {
19        unsafe {
20            // safety: strings sampled from valid utf-8 chars result in a valid utf-8 string
21            string.as_mut_vec().extend(self.sample_iter(rng).take(len));
22        }
23    }
24}
25
26mod tests {
27    #![allow(unused_imports)]
28    use super::*;
29
30    #[test]
31    fn test_dist_string() {
32        let mut rng = rand::rng();
33
34        let s = Numeric.sample_string(&mut rng, 20);
35        assert_eq!(s.len(), 20);
36        assert_eq!(str::from_utf8(s.as_bytes()), Ok(s.as_str()));
37    }
38}