use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[derive(Debug, Clone)]
pub struct ReservoirSampler {
capacity: usize,
rng: ChaCha8Rng,
}
impl ReservoirSampler {
pub fn new(capacity: usize) -> Self {
Self::seed(capacity, 42)
}
pub fn seed(capacity: usize, seed: u64) -> Self {
Self {
capacity,
rng: ChaCha8Rng::seed_from_u64(seed),
}
}
pub fn replacement_slot(&mut self, seen: usize) -> Option<usize> {
if self.capacity == 0 || seen <= self.capacity {
return None;
}
let draw = self.rng.random_range(0..seen);
(draw < self.capacity).then_some(draw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_replacement_while_the_reservoir_fills() {
let mut sampler = ReservoirSampler::new(3);
assert!((1..=3).all(|seen| sampler.replacement_slot(seen).is_none()));
}
#[test]
fn slots_stay_inside_the_reservoir_and_repeat_for_a_seed() {
let draws = |seed| {
let mut sampler = ReservoirSampler::seed(5, seed);
(6..500)
.map(|seen| sampler.replacement_slot(seen))
.collect::<Vec<_>>()
};
let first = draws(7);
assert!(first.iter().flatten().all(|&slot| slot < 5));
assert!(first.iter().any(Option::is_some));
assert_eq!(first, draws(7));
}
}