use std::time::{SystemTime, UNIX_EPOCH};
pub trait RandomSource {
fn gen_below(&mut self, max_exclusive: u32) -> u32;
}
pub struct SplitMix64 {
state: u64,
}
impl SplitMix64 {
pub fn new(seed: u64) -> Self {
Self { state: seed }
}
pub fn from_entropy() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15);
Self::new(nanos ^ 0x9E37_79B9_7F4A_7C15)
}
pub fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
impl RandomSource for SplitMix64 {
fn gen_below(&mut self, max_exclusive: u32) -> u32 {
assert!(max_exclusive > 0, "max_exclusive must be positive");
let m = u64::from(max_exclusive);
let limit = (u64::MAX / m) * m;
loop {
let value = self.next_u64();
if value < limit {
return (value % m) as u32;
}
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct OsRandom;
impl OsRandom {
pub fn new() -> Self {
Self
}
}
impl RandomSource for OsRandom {
fn gen_below(&mut self, max_exclusive: u32) -> u32 {
assert!(max_exclusive > 0, "max_exclusive must be positive");
let m = u64::from(max_exclusive);
let limit = (u64::MAX / m) * m;
loop {
let mut buf = [0u8; 8];
getrandom::getrandom(&mut buf).expect("OS CSPRNG (getrandom) failed");
let value = u64::from_le_bytes(buf);
if value < limit {
return (value % m) as u32;
}
}
}
}
pub fn pick<'a, T, R: RandomSource>(items: &'a [T], rng: &mut R) -> &'a T {
assert!(!items.is_empty(), "cannot pick from an empty slice");
let index = rng.gen_below(items.len() as u32) as usize;
&items[index]
}
pub fn digits<R: RandomSource>(length: usize, rng: &mut R) -> String {
let mut out = String::with_capacity(length);
for _ in 0..length {
let digit = rng.gen_below(10) as u8;
out.push((b'0' + digit) as char);
}
out
}
pub struct CodeBuilder<R: RandomSource> {
parts: Vec<String>,
rng: R,
}
impl<R: RandomSource> CodeBuilder<R> {
pub fn new(rng: R) -> Self {
Self {
parts: Vec::new(),
rng,
}
}
pub fn add(mut self, value: impl Into<String>) -> Self {
self.parts.push(value.into());
self
}
pub fn add_with<F: FnOnce(&mut R) -> String>(mut self, f: F) -> Self {
let value = f(&mut self.rng);
self.parts.push(value);
self
}
pub fn dash(self) -> Self {
self.add("-")
}
pub fn digits(self, length: usize) -> Self {
self.add_with(|rng| digits(length, rng))
}
pub fn nums(self, length: usize) -> Self {
self.digits(length)
}
pub fn build(self) -> String {
self.parts.concat()
}
}
pub fn code<R: RandomSource>(rng: R) -> CodeBuilder<R> {
CodeBuilder::new(rng)
}
#[cfg(test)]
mod tests {
use super::*;
struct Fixed(u32);
impl RandomSource for Fixed {
fn gen_below(&mut self, max_exclusive: u32) -> u32 {
self.0 % max_exclusive
}
}
struct Seq {
values: Vec<u32>,
index: usize,
}
impl RandomSource for Seq {
fn gen_below(&mut self, max_exclusive: u32) -> u32 {
let value = self.values[self.index % self.values.len()];
self.index += 1;
value % max_exclusive
}
}
#[test]
fn digits_are_deterministic() {
assert_eq!(digits(4, &mut Fixed(7)), "7777");
assert_eq!(
digits(
4,
&mut Seq {
values: vec![1, 2, 3, 4],
index: 0
}
),
"1234"
);
}
#[test]
fn dash_appends_single_hyphen() {
assert_eq!(code(Fixed(0)).add("ab").dash().add("cd").build(), "ab-cd");
}
#[test]
fn nums_is_alias_for_digits() {
assert_eq!(code(Fixed(5)).digits(3).build(), code(Fixed(5)).nums(3).build());
}
#[test]
fn composes_full_code() {
assert_eq!(code(Fixed(0)).add("teva").dash().digits(4).build(), "teva-0000");
}
#[test]
fn seeded_default_stays_in_range() {
let mut rng = SplitMix64::new(42);
for _ in 0..1000 {
assert!(rng.gen_below(10) < 10);
}
}
#[test]
fn os_random_stays_in_range() {
let mut rng = OsRandom::new();
for _ in 0..1000 {
assert!(rng.gen_below(10) < 10);
}
}
#[test]
fn digits_empty_for_zero_length() {
assert_eq!(digits(0, &mut Fixed(7)), "");
}
#[test]
fn digits_maps_every_index_to_its_decimal() {
assert_eq!(
digits(
10,
&mut Seq {
values: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
index: 0
}
),
"0123456789"
);
}
#[test]
fn empty_builder_builds_empty_string() {
assert_eq!(code(Fixed(0)).build(), "");
}
#[test]
fn builder_preserves_empty_fragments() {
assert_eq!(code(Fixed(0)).add("a").add("").add("b").build(), "ab");
}
#[test]
fn builder_preserves_fragment_order() {
assert_eq!(
code(Fixed(0)).add("a").dash().add("b").dash().add("c").build(),
"a-b-c"
);
}
#[test]
fn pick_returns_singleton_element() {
assert_eq!(*pick(&["solo"], &mut Fixed(0)), "solo");
}
#[test]
fn pick_uses_index_from_source() {
assert_eq!(*pick(&["a", "b", "c", "d"], &mut Fixed(2)), "c");
}
#[test]
#[should_panic(expected = "cannot pick from an empty slice")]
fn pick_panics_on_empty_slice() {
let empty: [u8; 0] = [];
pick(&empty, &mut Fixed(0));
}
#[test]
#[should_panic(expected = "max_exclusive must be positive")]
fn splitmix_panics_on_zero_bound() {
SplitMix64::new(1).gen_below(0);
}
#[test]
#[should_panic(expected = "max_exclusive must be positive")]
fn os_random_panics_on_zero_bound() {
OsRandom::new().gen_below(0);
}
#[test]
fn gen_below_one_is_always_zero() {
let mut rng = SplitMix64::new(123);
for _ in 0..100 {
assert_eq!(rng.gen_below(1), 0);
}
}
#[test]
fn splitmix_is_reproducible_for_a_seed() {
let draw = |seed: u64| {
let mut rng = SplitMix64::new(seed);
(0..16).map(|_| rng.gen_below(1000)).collect::<Vec<_>>()
};
assert_eq!(draw(42), draw(42));
assert_ne!(draw(42), draw(43));
}
#[test]
fn splitmix_next_u64_is_deterministic() {
let mut a = SplitMix64::new(0);
let mut b = SplitMix64::new(0);
for _ in 0..8 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn gen_below_covers_full_small_range() {
let mut rng = SplitMix64::new(7);
let mut seen = [false; 4];
for _ in 0..2000 {
seen[rng.gen_below(4) as usize] = true;
}
assert!(seen.iter().all(|&hit| hit));
}
}