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}