use crate::common::{validation, Mergeable, Sketch, SketchError};
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct CountMinSketch {
width: usize,
mask: usize,
depth: usize,
table: Vec<u64>,
epsilon: f64,
delta: f64,
}
impl CountMinSketch {
pub fn new(epsilon: f64, delta: 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(),
});
}
if delta <= 0.0 || delta >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "delta".to_string(),
value: delta.to_string(),
constraint: "must be in (0, 1)".to_string(),
});
}
let width_min = (2.0 / epsilon).ceil() as usize;
let width = width_min.next_power_of_two(); let mask = width - 1;
let depth = (1.0 / delta).ln().ceil() as usize;
let depth = depth.max(1);
let table = vec![0u64; depth * width];
Ok(CountMinSketch {
width,
mask,
depth,
table,
epsilon,
delta,
})
}
#[inline]
pub fn update<T: Hash>(&mut self, item: &T) {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
let width = self.width;
let mask = self.mask;
let depth = self.depth;
for row_idx in 0..depth {
let hash = hasher.finish();
let col_idx = (hash as usize) & mask;
let idx = row_idx * width + col_idx;
unsafe {
*self.table.get_unchecked_mut(idx) =
self.table.get_unchecked(idx).saturating_add(1);
}
hasher.write(&[0x7B]);
}
}
#[inline]
pub fn estimate<T: Hash>(&self, item: &T) -> u64 {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
let width = self.width;
let mask = self.mask;
let depth = self.depth;
let mut min_count = u64::MAX;
for row_idx in 0..depth {
let hash = hasher.finish();
let col_idx = (hash as usize) & mask;
let idx = row_idx * width + col_idx;
let count = unsafe { *self.table.get_unchecked(idx) };
min_count = min_count.min(count);
hasher.write(&[0x7B]);
}
if min_count == u64::MAX {
0
} else {
min_count
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn depth(&self) -> usize {
self.depth
}
#[inline]
pub fn epsilon(&self) -> f64 {
self.epsilon
}
#[inline]
pub fn delta(&self) -> f64 {
self.delta
}
}
impl Sketch for CountMinSketch {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
CountMinSketch::update(self, item);
}
fn estimate(&self) -> f64 {
0.0
}
fn is_empty(&self) -> bool {
self.table.iter().all(|&count| count == 0)
}
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.width.to_le_bytes());
bytes.extend_from_slice(&self.depth.to_le_bytes());
bytes.extend_from_slice(&self.epsilon.to_le_bytes());
bytes.extend_from_slice(&self.delta.to_le_bytes());
for &count in &self.table {
bytes.extend_from_slice(&count.to_le_bytes());
}
bytes
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
validation::validate_byte_size(bytes.len())?;
validation::validate_min_size(bytes.len(), 32)?;
let mut offset = 0;
let width = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid width".to_string()))?,
);
offset += 8;
let depth = usize::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid depth".to_string()))?,
);
offset += 8;
validation::validate_width_depth(width as u32, depth as u32)?;
let epsilon = f64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid epsilon".to_string()))?,
);
offset += 8;
let delta = f64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid delta".to_string()))?,
);
offset += 8;
validation::validate_probability(epsilon, "epsilon")?;
validation::validate_probability(delta, "delta")?;
let mask = width - 1;
let expected_table_size = depth * width * 8;
if bytes.len() < offset + expected_table_size {
return Err(SketchError::DeserializationError(
"insufficient bytes for table".to_string(),
));
}
let mut table = Vec::with_capacity(depth * width);
for _ in 0..(depth * width) {
let count = u64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.map_err(|_| SketchError::DeserializationError("invalid count".to_string()))?,
);
table.push(count);
offset += 8;
}
Ok(CountMinSketch {
width,
mask,
depth,
table,
epsilon,
delta,
})
}
}
impl Mergeable for CountMinSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.width != other.width {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"width mismatch: {} vs {} (different epsilon parameters)",
self.width, other.width
),
});
}
if self.depth != other.depth {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"depth mismatch: {} vs {} (different delta parameters)",
self.depth, other.depth
),
});
}
for (a, &b) in self.table.iter_mut().zip(other.table.iter()) {
*a = a.saturating_add(b);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_construction() {
let cms = CountMinSketch::new(0.01, 0.01).unwrap();
assert!(cms.width() > 0);
assert!(cms.depth() > 0);
assert!(cms.width().is_power_of_two());
}
#[test]
fn test_dimension_calculation() {
let cms = CountMinSketch::new(0.01, 0.01).unwrap();
assert_eq!(cms.width(), 256);
assert_eq!(cms.depth(), 5);
}
#[test]
fn test_update_and_estimate() {
let mut cms = CountMinSketch::new(0.01, 0.01).unwrap();
cms.update(&"test");
assert_eq!(cms.estimate(&"test"), 1);
}
#[test]
fn test_never_underestimates() {
let mut cms = CountMinSketch::new(0.01, 0.01).unwrap();
for _ in 0..100 {
cms.update(&"item");
}
let estimate = cms.estimate(&"item");
assert!(estimate >= 100);
}
#[test]
fn test_merge_basic() {
let mut cms1 = CountMinSketch::new(0.01, 0.01).unwrap();
let mut cms2 = CountMinSketch::new(0.01, 0.01).unwrap();
cms1.update(&"a");
cms2.update(&"a");
cms1.merge(&cms2).unwrap();
assert!(cms1.estimate(&"a") >= 2);
}
#[test]
fn test_merge_incompatible() {
let mut cms1 = CountMinSketch::new(0.01, 0.01).unwrap();
let cms2 = CountMinSketch::new(0.001, 0.01).unwrap();
let result = cms1.merge(&cms2);
assert!(result.is_err());
}
}