use crate::errors::DataProfilerError;
use super::reservoir::ReservoirSampler;
use super::strategies::SamplingStrategy;
#[derive(Debug)]
struct SampleBuffer<R> {
capacity: usize,
rows: Vec<R>,
sampler: ReservoirSampler,
seen: usize,
}
impl<R> SampleBuffer<R> {
fn new(capacity: usize) -> Self {
Self {
capacity,
rows: Vec::new(),
sampler: ReservoirSampler::new(capacity),
seen: 0,
}
}
fn offer(&mut self, values: R) {
self.seen += 1;
if self.capacity == 0 {
return;
}
if self.rows.len() < self.capacity {
self.rows.push(values);
return;
}
if let Some(position) = self.sampler.replacement_slot(self.seen) {
self.rows[position] = values;
}
}
fn take(&mut self) -> Vec<R> {
std::mem::take(&mut self.rows)
}
}
#[derive(Debug)]
pub struct RowSampler<R = Vec<String>> {
interval: Option<usize>,
buffer: Option<SampleBuffer<R>>,
iterated: usize,
accepted: usize,
}
impl RowSampler {
pub fn new(strategy: &SamplingStrategy) -> Result<Self, DataProfilerError> {
Self::for_rows(strategy)
}
}
impl<R> RowSampler<R> {
pub fn for_rows(strategy: &SamplingStrategy) -> Result<Self, DataProfilerError> {
let (interval, buffer) = match strategy {
SamplingStrategy::None => (None, None),
SamplingStrategy::Random { size } | SamplingStrategy::Reservoir { size } => {
(None, Some(SampleBuffer::new(*size)))
}
SamplingStrategy::Systematic { interval } => {
((*interval > 1).then_some(*interval), None)
}
};
Ok(Self {
interval,
buffer,
iterated: 0,
accepted: 0,
})
}
pub fn is_buffered(&self) -> bool {
self.buffer.is_some()
}
pub fn is_noop(&self) -> bool {
self.interval.is_none() && self.buffer.is_none()
}
pub fn accept(&mut self) -> bool {
let index = self.iterated;
self.iterated += 1;
if self
.interval
.is_some_and(|interval| !index.is_multiple_of(interval))
{
return false;
}
if self.buffer.is_none() {
self.accepted += 1;
}
true
}
pub fn offer(&mut self, values: R) {
if let Some(buffer) = self.buffer.as_mut() {
buffer.offer(values);
}
}
pub fn take_sample(&mut self) -> Vec<R> {
match self.buffer.as_mut() {
Some(buffer) => {
let rows = buffer.take();
self.accepted += rows.len();
rows
}
None => Vec::new(),
}
}
pub fn iterated_rows(&self) -> usize {
self.iterated
}
pub fn sampled_rows(&self) -> usize {
self.accepted
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(strategy: SamplingStrategy, rows: usize) -> (usize, usize) {
let mut sampler = RowSampler::new(&strategy).expect("valid strategy");
for i in 0..rows {
if sampler.accept() && sampler.is_buffered() {
sampler.offer(vec![i.to_string()]);
}
}
let final_rows = sampler.take_sample().len();
(sampler.sampled_rows(), final_rows)
}
#[test]
fn reservoir_yields_exactly_its_size() {
let (sampled, buffered) = run(SamplingStrategy::Reservoir { size: 10 }, 100);
assert_eq!(buffered, 10);
assert_eq!(sampled, 10);
}
#[test]
fn reservoir_smaller_than_its_size_keeps_every_row() {
let (sampled, buffered) = run(SamplingStrategy::Reservoir { size: 50 }, 20);
assert_eq!(buffered, 20, "a short stream cannot fill the reservoir");
assert_eq!(sampled, 20);
}
#[test]
fn random_matches_reservoir_semantics() {
let (_, buffered) = run(SamplingStrategy::Random { size: 25 }, 500);
assert_eq!(buffered, 25);
}
#[test]
fn systematic_takes_every_nth_row() {
let (sampled, _) = run(SamplingStrategy::Systematic { interval: 10 }, 100);
assert_eq!(sampled, 10);
}
#[test]
fn systematic_with_interval_zero_or_one_keeps_every_row() {
for interval in [0, 1] {
let (sampled, _) = run(SamplingStrategy::Systematic { interval }, 37);
assert_eq!(sampled, 37, "interval {interval}");
}
}
#[test]
fn none_is_a_noop() {
let sampler = RowSampler::new(&SamplingStrategy::None).unwrap();
assert!(sampler.is_noop());
assert!(!sampler.is_buffered());
}
}