use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
use crate::hash::seeded_hash;
use crate::heap::{HeapEntry, push_bounded};
use crate::{BucketKey, GroupKey};
type RankHashKey = (u64, u64);
pub fn reservoir_sample<R>(
items: impl Iterator<Item = R>,
count: usize,
seed: u64,
key_fn: impl Fn(&R) -> &str,
) -> (Vec<R>, usize) {
let mut heap: BinaryHeap<HeapEntry<u64, R>> =
BinaryHeap::with_capacity(count.saturating_add(1));
let mut total = 0usize;
for record in items {
let key = seeded_hash(seed, key_fn(&record));
let index = total;
total += 1;
push_bounded(&mut heap, count, HeapEntry { key, index, record });
}
let mut kept: Vec<HeapEntry<u64, R>> = heap.into_vec();
kept.sort_by_key(|e| e.index);
(kept.into_iter().map(|e| e.record).collect(), total)
}
pub struct GroupAwareFillResult<R> {
pub kept: Vec<R>,
pub total: usize,
pub quota_candidates: usize,
pub bucket_not_in_quota: usize,
pub distinct_groups_kept: usize,
pub max_group_share_in_any_bucket: Option<f64>,
}
pub fn group_aware_fill<R>(
items: impl Iterator<Item = R>,
quotas: &BTreeMap<BucketKey, usize>,
seed: u64,
bucket_key_fn: impl Fn(&R) -> BucketKey,
group_key_fn: impl Fn(&R) -> GroupKey,
) -> GroupAwareFillResult<R> {
let mut total = 0usize;
let mut bucket_not_in_quota = 0usize;
let mut quota_candidates = 0usize;
let mut group_rank: HashMap<(BucketKey, GroupKey), u64> = HashMap::new();
let mut heaps: HashMap<BucketKey, BinaryHeap<HeapEntry<RankHashKey, R>>> = HashMap::new();
for record in items {
let index = total;
total += 1;
let bucket = bucket_key_fn(&record);
let Some("a) = quotas.get(&bucket) else {
bucket_not_in_quota += 1;
continue;
};
quota_candidates += 1;
let group = group_key_fn(&record);
let counter = group_rank
.entry((bucket.clone(), group.clone()))
.or_insert(0);
let rank = *counter;
*counter += 1;
let key = (rank, seeded_hash(seed, &group));
push_bounded(
heaps.entry(bucket).or_default(),
quota,
HeapEntry { key, index, record },
);
}
let mut kept: Vec<HeapEntry<RankHashKey, R>> =
heaps.into_values().flat_map(|h| h.into_vec()).collect();
kept.sort_by_key(|e| e.index);
let mut per_bucket_groups: HashMap<BucketKey, HashMap<GroupKey, usize>> = HashMap::new();
for entry in &kept {
let bucket = bucket_key_fn(&entry.record);
let group = group_key_fn(&entry.record);
*per_bucket_groups
.entry(bucket)
.or_default()
.entry(group)
.or_default() += 1;
}
let max_group_share_in_any_bucket = per_bucket_groups
.values()
.filter_map(|groups| {
let bucket_total: usize = groups.values().sum();
(bucket_total >= 2)
.then(|| groups.values().copied().max().unwrap_or(0) as f64 / bucket_total as f64)
})
.fold(None::<f64>, |acc, share| {
Some(acc.map_or(share, |a| a.max(share)))
});
let distinct_groups_kept = kept
.iter()
.map(|e| group_key_fn(&e.record))
.collect::<HashSet<_>>()
.len();
GroupAwareFillResult {
kept: kept.into_iter().map(|e| e.record).collect(),
total,
quota_candidates,
bucket_not_in_quota,
distinct_groups_kept,
max_group_share_in_any_bucket,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reservoir_sample_restores_input_order_and_is_deterministic() {
let items: Vec<String> = (0..20).map(|n: u32| n.to_string()).collect();
let (kept_a, total_a) = reservoir_sample(items.clone().into_iter(), 5, 7, |s| s.as_str());
let (kept_b, total_b) = reservoir_sample(items.into_iter(), 5, 7, |s| s.as_str());
assert_eq!(total_a, 20);
assert_eq!(total_b, 20);
assert_eq!(kept_a, kept_b);
assert_eq!(kept_a.len(), 5);
let indices: Vec<u32> = kept_a.iter().map(|s| s.parse().unwrap()).collect();
let mut sorted = indices.clone();
sorted.sort();
assert_eq!(indices, sorted, "result should be in original input order");
}
#[test]
fn group_aware_fill_does_not_let_one_group_starve_out_another() {
let mut items: Vec<(&str, &str)> = (0..10).map(|_| ("bucket", "root-a")).collect();
items.push(("bucket", "root-b"));
let mut quotas = BTreeMap::new();
quotas.insert("bucket".to_string(), 2);
let result = group_aware_fill(
items.into_iter(),
"as,
1,
|(bucket, _)| bucket.to_string(),
|(_, group)| group.to_string(),
);
assert_eq!(result.kept.len(), 2);
let groups: HashSet<&str> = result.kept.iter().map(|(_, g)| *g).collect();
assert!(
groups.contains("root-b"),
"the sole root-b item must survive, not be starved out"
);
}
#[test]
fn group_aware_fill_reports_bucket_not_in_quota() {
let items = vec![("known", 1), ("unknown", 2)];
let mut quotas = BTreeMap::new();
quotas.insert("known".to_string(), 10);
let result = group_aware_fill(
items.into_iter(),
"as,
0,
|(bucket, _)| bucket.to_string(),
|(_, group)| group.to_string(),
);
assert_eq!(result.total, 2);
assert_eq!(result.quota_candidates, 1);
assert_eq!(result.bucket_not_in_quota, 1);
assert_eq!(result.kept.len(), 1);
}
#[test]
fn group_aware_fill_with_no_quotas_drops_every_item() {
let items = vec![("a", 1), ("b", 2), ("c", 3)];
let quotas: BTreeMap<String, usize> = BTreeMap::new();
let result = group_aware_fill(
items.into_iter(),
"as,
0,
|(bucket, _)| bucket.to_string(),
|(_, group)| group.to_string(),
);
assert_eq!(result.total, 3);
assert_eq!(result.quota_candidates, 0);
assert_eq!(result.bucket_not_in_quota, 3);
assert!(result.kept.is_empty());
assert_eq!(result.distinct_groups_kept, 0);
assert_eq!(result.max_group_share_in_any_bucket, None);
}
#[test]
fn group_aware_fill_on_empty_input_is_a_safe_no_op() {
let items: Vec<(&str, &str)> = Vec::new();
let mut quotas = BTreeMap::new();
quotas.insert("bucket".to_string(), 5);
let result = group_aware_fill(
items.into_iter(),
"as,
0,
|(bucket, _)| bucket.to_string(),
|(_, group)| group.to_string(),
);
assert_eq!(result.total, 0);
assert!(result.kept.is_empty());
}
#[test]
fn reservoir_sample_on_empty_input_returns_nothing() {
let items: Vec<String> = Vec::new();
let (kept, total) = reservoir_sample(items.into_iter(), 5, 1, |s| s.as_str());
assert_eq!(total, 0);
assert!(kept.is_empty());
}
}