use crate::common::{Mergeable, Sketch, SketchError};
use std::collections::HashMap;
use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct SpaceSaving<T: Hash + Eq + Clone> {
capacity: usize,
items: HashMap<T, (u64, u64)>,
stream_length: u64,
epsilon: f64,
}
impl<T: Hash + Eq + Clone> SpaceSaving<T> {
pub fn new(epsilon: f64) -> Result<Self, SketchError> {
if epsilon <= 0.0 || epsilon >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "epsilon".to_string(),
value: epsilon.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let capacity = (1.0 / epsilon).ceil() as usize;
let capacity = capacity.max(2);
Ok(Self {
capacity,
items: HashMap::with_capacity(capacity),
stream_length: 0,
epsilon,
})
}
pub fn with_capacity(capacity: usize) -> Result<Self, SketchError> {
if capacity < 2 {
return Err(SketchError::InvalidParameter {
param: "capacity".to_string(),
value: capacity.to_string(),
constraint: "must be >= 2".to_string(),
});
}
let epsilon = 1.0 / capacity as f64;
Ok(Self {
capacity,
items: HashMap::with_capacity(capacity),
stream_length: 0,
epsilon,
})
}
#[inline]
pub fn update(&mut self, item: T) {
self.stream_length += 1;
if let Some((count, _error)) = self.items.get_mut(&item) {
*count += 1;
return;
}
if self.items.len() < self.capacity {
self.items.insert(item, (1, 0));
return;
}
let min_count = self
.items
.values()
.map(|(count, _)| *count)
.min()
.unwrap_or(0);
let min_item = self
.items
.iter()
.find(|(_, (count, _))| *count == min_count)
.map(|(k, _)| k.clone());
if let Some(old_item) = min_item {
self.items.remove(&old_item);
self.items.insert(item, (min_count + 1, min_count));
}
}
#[inline]
pub fn estimate(&self, item: &T) -> Option<(u64, u64)> {
self.items.get(item).map(|&(count, error)| {
let lower = count.saturating_sub(error);
let upper = count;
(lower, upper)
})
}
pub fn heavy_hitters(&self, threshold: f64) -> Vec<(T, u64, u64)> {
let min_count = (threshold * self.stream_length as f64).ceil() as u64;
let mut result: Vec<_> = self
.items
.iter()
.filter_map(|(item, &(count, error))| {
if count >= min_count {
let lower = count.saturating_sub(error);
Some((item.clone(), lower, count))
} else {
None
}
})
.collect();
result.sort_by(|a, b| b.2.cmp(&a.2));
result
}
pub fn top_k(&self, k: usize) -> Vec<(T, u64, u64)> {
let mut result: Vec<_> = self
.items
.iter()
.map(|(item, &(count, error))| {
let lower = count.saturating_sub(error);
(item.clone(), lower, count)
})
.collect();
result.sort_by(|a, b| b.2.cmp(&a.2));
result.truncate(k);
result
}
#[inline]
pub fn capacity(&self) -> usize {
self.capacity
}
#[inline]
pub fn stream_length(&self) -> u64 {
self.stream_length
}
#[inline]
pub fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline]
pub fn num_items(&self) -> usize {
self.items.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[inline]
pub fn max_error(&self) -> u64 {
(self.stream_length as f64 * self.epsilon).ceil() as u64
}
}
impl<T: Hash + Eq + Clone> SpaceSaving<T> {
pub fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.capacity != other.capacity {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"capacity mismatch: {} vs {} (different epsilon parameters)",
self.capacity, other.capacity
),
});
}
let other_max_error = other.max_error();
let self_max_error = self.max_error();
for (item, &(other_count, other_error)) in &other.items {
if let Some((count, error)) = self.items.get_mut(item) {
*count += other_count;
*error += other_error;
} else {
let new_error = other_error + self_max_error;
self.items.insert(item.clone(), (other_count, new_error));
}
}
for (item, (count, error)) in self.items.iter_mut() {
if !other.items.contains_key(item) {
*error += other_max_error;
*error = (*error).min(*count);
}
}
self.stream_length += other.stream_length;
while self.items.len() > self.capacity {
let min_item = self
.items
.iter()
.min_by_key(|(_, (count, _))| *count)
.map(|(k, _)| k.clone());
if let Some(item) = min_item {
self.items.remove(&item);
} else {
break;
}
}
Ok(())
}
}
impl<T: Hash + Eq + Clone + 'static> Sketch for SpaceSaving<T> {
type Item = T;
fn update(&mut self, item: &Self::Item) {
SpaceSaving::update(self, item.clone());
}
fn estimate(&self) -> f64 {
self.items
.values()
.map(|(count, _)| *count)
.max()
.unwrap_or(0) as f64
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.capacity.to_le_bytes());
bytes.extend_from_slice(&self.stream_length.to_le_bytes());
bytes.extend_from_slice(&self.epsilon.to_le_bytes());
bytes.extend_from_slice(&self.items.len().to_le_bytes());
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 32 {
return Err(SketchError::DeserializationError(
"insufficient bytes for header".to_string(),
));
}
let mut offset = 0;
let capacity = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid capacity".to_string()))?,
);
offset += 8;
let stream_length =
u64::from_le_bytes(bytes[offset..offset + 8].try_into().map_err(|_| {
SketchError::DeserializationError("invalid stream_length".to_string())
})?);
offset += 8;
let epsilon = f64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid epsilon".to_string()))?,
);
offset += 8;
let num_items = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid num_items".to_string()))?,
);
if num_items > 0 {
return Err(SketchError::DeserializationError(
"generic deserialization of items not supported; use serde feature".to_string(),
));
}
Ok(Self {
capacity,
items: HashMap::with_capacity(capacity),
stream_length,
epsilon,
})
}
}
impl<T: Hash + Eq + Clone + 'static> Mergeable for SpaceSaving<T> {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
SpaceSaving::merge(self, other)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_basic_insertion() {
let mut sketch: SpaceSaving<String> = SpaceSaving::new(0.1).unwrap();
sketch.update("apple".to_string());
sketch.update("banana".to_string());
sketch.update("cherry".to_string());
assert_eq!(sketch.num_items(), 3);
let (lower, upper) = sketch.estimate(&"apple".to_string()).unwrap();
assert_eq!(lower, 1);
assert_eq!(upper, 1);
let (lower, upper) = sketch.estimate(&"banana".to_string()).unwrap();
assert_eq!(lower, 1);
assert_eq!(upper, 1);
sketch.update("apple".to_string());
let (lower, upper) = sketch.estimate(&"apple".to_string()).unwrap();
assert_eq!(lower, 2);
assert_eq!(upper, 2);
}
#[test]
fn test_replacement_at_capacity() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::with_capacity(3).unwrap();
sketch.update(1);
sketch.update(2);
sketch.update(3);
assert_eq!(sketch.num_items(), 3);
for i in 1..=3 {
let (lower, upper) = sketch.estimate(&i).unwrap();
assert_eq!(lower, 1);
assert_eq!(upper, 1);
}
sketch.update(4);
assert_eq!(sketch.num_items(), 3);
let (lower, upper) = sketch.estimate(&4).unwrap();
assert_eq!(upper, 2); assert_eq!(lower, 1);
let tracked: HashSet<_> = (1..=4).filter(|i| sketch.estimate(i).is_some()).collect();
assert_eq!(tracked.len(), 3);
assert!(tracked.contains(&4));
}
#[test]
fn test_error_bounds() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::with_capacity(5).unwrap();
for _ in 0..100 {
sketch.update(1); }
for _ in 0..50 {
sketch.update(2); }
for _ in 0..25 {
sketch.update(3); }
let (lower1, upper1) = sketch.estimate(&1).unwrap();
assert!(
lower1 <= 100 && upper1 >= 100,
"Item 1: {} <= 100 <= {}",
lower1,
upper1
);
let (lower2, upper2) = sketch.estimate(&2).unwrap();
assert!(
lower2 <= 50 && upper2 >= 50,
"Item 2: {} <= 50 <= {}",
lower2,
upper2
);
let (lower3, upper3) = sketch.estimate(&3).unwrap();
assert!(
lower3 <= 25 && upper3 >= 25,
"Item 3: {} <= 25 <= {}",
lower3,
upper3
);
}
#[test]
fn test_no_false_negatives() {
let epsilon = 0.01; let mut sketch: SpaceSaving<u32> = SpaceSaving::new(epsilon).unwrap();
let n = 10000u64;
let heavy_hitter_count = (n as f64 * 0.05) as u64;
for _ in 0..heavy_hitter_count {
sketch.update(42);
}
for i in 0..(n - heavy_hitter_count) {
sketch.update(i as u32 + 1000); }
let estimate = sketch.estimate(&42);
assert!(
estimate.is_some(),
"Heavy hitter (5% frequency) must be tracked in sketch with 1% epsilon"
);
let heavy = sketch.heavy_hitters(0.02); let heavy_items: HashSet<_> = heavy.iter().map(|(item, _, _)| *item).collect();
assert!(
heavy_items.contains(&42),
"Heavy hitter must appear in heavy_hitters result"
);
}
#[test]
fn test_merge_correctness() {
let mut sketch1: SpaceSaving<u32> = SpaceSaving::with_capacity(10).unwrap();
let mut sketch2: SpaceSaving<u32> = SpaceSaving::with_capacity(10).unwrap();
for _ in 0..50 {
sketch1.update(1);
}
for _ in 0..30 {
sketch1.update(2);
}
for _ in 0..40 {
sketch2.update(1);
}
for _ in 0..20 {
sketch2.update(3);
}
sketch1.merge(&sketch2).unwrap();
assert_eq!(
sketch1.stream_length(),
50 + 30 + 40 + 20,
"Stream length should be sum of both"
);
let (lower1, upper1) = sketch1.estimate(&1).unwrap();
assert!(
lower1 <= 90 && upper1 >= 90,
"Merged item 1: {} <= 90 <= {}",
lower1,
upper1
);
if let Some((lower2, upper2)) = sketch1.estimate(&2) {
assert!(
lower2 <= 30 && upper2 >= 30,
"Merged item 2: {} <= 30 <= {}",
lower2,
upper2
);
}
if let Some((lower3, upper3)) = sketch1.estimate(&3) {
assert!(
lower3 <= 20 && upper3 >= 20,
"Merged item 3: {} <= 20 <= {}",
lower3,
upper3
);
}
}
#[test]
fn test_merge_incompatible() {
let mut sketch1: SpaceSaving<u32> = SpaceSaving::with_capacity(10).unwrap();
let sketch2: SpaceSaving<u32> = SpaceSaving::with_capacity(20).unwrap();
let result = sketch1.merge(&sketch2);
assert!(result.is_err());
if let Err(SketchError::IncompatibleSketches { reason }) = result {
assert!(reason.contains("capacity mismatch"));
} else {
panic!("Expected IncompatibleSketches error");
}
}
#[test]
fn test_zipf_distribution() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::new(0.01).unwrap();
let mut true_counts = HashMap::new();
let scale = 1000.0;
for i in 1..=100u32 {
let count = (scale / i as f64).ceil() as u64;
true_counts.insert(i, count);
for _ in 0..count {
sketch.update(i);
}
}
for i in 1..=5u32 {
let true_count = true_counts[&i];
if let Some((lower, upper)) = sketch.estimate(&i) {
assert!(
lower <= true_count && upper >= true_count,
"Zipf item {}: {} <= {} <= {}",
i,
lower,
true_count,
upper
);
} else {
panic!("Top Zipf item {} should be tracked", i);
}
}
let heavy = sketch.heavy_hitters(0.05);
assert!(
!heavy.is_empty(),
"Should have heavy hitters in Zipf distribution"
);
let heavy_items: HashSet<_> = heavy.iter().map(|(item, _, _)| *item).collect();
assert!(heavy_items.contains(&1), "Item 1 should be a heavy hitter");
}
#[test]
fn test_serialization() {
let sketch: SpaceSaving<String> = SpaceSaving::new(0.1).unwrap();
let bytes = sketch.serialize();
assert!(!bytes.is_empty());
let restored: SpaceSaving<String> = SpaceSaving::deserialize(&bytes).unwrap();
assert_eq!(restored.capacity(), sketch.capacity());
assert_eq!(restored.stream_length(), sketch.stream_length());
assert!((restored.epsilon() - sketch.epsilon()).abs() < 1e-10);
}
#[test]
fn test_parameter_validation() {
assert!(SpaceSaving::<u32>::new(0.0).is_err());
assert!(SpaceSaving::<u32>::new(-0.1).is_err());
assert!(SpaceSaving::<u32>::new(1.0).is_err());
assert!(SpaceSaving::<u32>::new(1.5).is_err());
assert!(SpaceSaving::<u32>::new(0.5).is_ok());
assert!(SpaceSaving::<u32>::new(0.001).is_ok());
assert!(SpaceSaving::<u32>::with_capacity(0).is_err());
assert!(SpaceSaving::<u32>::with_capacity(1).is_err());
assert!(SpaceSaving::<u32>::with_capacity(2).is_ok());
assert!(SpaceSaving::<u32>::with_capacity(100).is_ok());
}
#[test]
fn test_empty_sketch() {
let sketch: SpaceSaving<String> = SpaceSaving::new(0.1).unwrap();
assert!(sketch.is_empty());
assert_eq!(sketch.num_items(), 0);
assert_eq!(sketch.stream_length(), 0);
assert!(sketch.estimate(&"anything".to_string()).is_none());
assert!(sketch.heavy_hitters(0.1).is_empty());
assert!(sketch.top_k(10).is_empty());
}
#[test]
fn test_top_k() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::with_capacity(20).unwrap();
for i in 1..=10u32 {
for _ in 0..(i * 10) {
sketch.update(i);
}
}
let top3 = sketch.top_k(3);
assert_eq!(top3.len(), 3);
assert_eq!(top3[0].0, 10);
assert_eq!(top3[1].0, 9);
assert_eq!(top3[2].0, 8);
}
#[test]
fn test_sketch_trait() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::new(0.1).unwrap();
<SpaceSaving<u32> as Sketch>::update(&mut sketch, &42);
<SpaceSaving<u32> as Sketch>::update(&mut sketch, &42);
<SpaceSaving<u32> as Sketch>::update(&mut sketch, &42);
assert!(!<SpaceSaving<u32> as Sketch>::is_empty(&sketch));
assert_eq!(<SpaceSaving<u32> as Sketch>::estimate(&sketch), 3.0);
}
#[test]
fn test_mergeable_trait() {
let mut sketch1: SpaceSaving<u32> = SpaceSaving::new(0.1).unwrap();
let mut sketch2: SpaceSaving<u32> = SpaceSaving::new(0.1).unwrap();
sketch1.update(1);
sketch2.update(1);
<SpaceSaving<u32> as Mergeable>::merge(&mut sketch1, &sketch2).unwrap();
assert_eq!(sketch1.stream_length(), 2);
}
#[test]
fn test_max_error() {
let mut sketch: SpaceSaving<u32> = SpaceSaving::new(0.1).unwrap();
for i in 0..1000 {
sketch.update(i);
}
let max_err = sketch.max_error();
assert!(
max_err <= 100 + 1,
"Max error should be bounded by N * epsilon"
);
}
#[test]
fn test_capacity_calculation() {
let sketch1: SpaceSaving<u32> = SpaceSaving::new(0.1).unwrap();
assert_eq!(sketch1.capacity(), 10);
let sketch2: SpaceSaving<u32> = SpaceSaving::new(0.01).unwrap();
assert_eq!(sketch2.capacity(), 100);
let sketch3: SpaceSaving<u32> = SpaceSaving::new(0.001).unwrap();
assert_eq!(sketch3.capacity(), 1000);
}
#[test]
fn test_clone() {
let mut sketch: SpaceSaving<String> = SpaceSaving::new(0.1).unwrap();
sketch.update("test".to_string());
let cloned = sketch.clone();
assert_eq!(cloned.capacity(), sketch.capacity());
assert_eq!(cloned.stream_length(), sketch.stream_length());
assert_eq!(
cloned.estimate(&"test".to_string()),
sketch.estimate(&"test".to_string())
);
}
}