use crate::error::{Result, SketchError};
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug)]
pub struct ThetaSketch {
lg_k: u8,
k: usize,
entries: HashSet<u64>,
theta: u64,
seed: u64,
}
impl ThetaSketch {
const MIN_LG_K: u8 = 4;
const MAX_LG_K: u8 = 26;
const DEFAULT_SEED: u64 = 9001;
pub fn new(lg_k: u8) -> Result<Self> {
if !(Self::MIN_LG_K..=Self::MAX_LG_K).contains(&lg_k) {
return Err(SketchError::InvalidParameter {
param: "lg_k".to_string(),
value: lg_k.to_string(),
constraint: format!("must be in range [{}, {}]", Self::MIN_LG_K, Self::MAX_LG_K),
});
}
let k = 1_usize << lg_k;
Ok(Self {
lg_k,
k,
entries: HashSet::with_capacity(k),
theta: u64::MAX,
seed: Self::DEFAULT_SEED,
})
}
pub fn with_seed(lg_k: u8, seed: u64) -> Result<Self> {
let mut sketch = Self::new(lg_k)?;
sketch.seed = seed;
Ok(sketch)
}
pub fn update<T: Hash>(&mut self, item: &T) {
let hash = self.hash_item(item);
if hash < self.theta {
self.entries.insert(hash);
if self.entries.len() > self.k {
self.rebuild_with_lower_theta();
}
}
}
pub fn estimate(&self) -> f64 {
if self.entries.is_empty() {
return 0.0;
}
if self.theta == u64::MAX {
self.entries.len() as f64
} else {
let count = self.entries.len() as f64;
let scale = u64::MAX as f64 / self.theta as f64;
count * scale
}
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn num_retained(&self) -> usize {
self.entries.len()
}
pub fn get_theta(&self) -> u64 {
self.theta
}
pub fn capacity(&self) -> usize {
self.k
}
pub fn union(&self, other: &Self) -> Result<Self> {
self.check_compatibility(other)?;
let new_theta = self.theta.min(other.theta);
let mut new_entries = HashSet::with_capacity(self.k);
for &hash in &self.entries {
if hash < new_theta {
new_entries.insert(hash);
}
}
for &hash in &other.entries {
if hash < new_theta {
new_entries.insert(hash);
}
}
Ok(Self {
lg_k: self.lg_k,
k: self.k,
entries: new_entries,
theta: new_theta,
seed: self.seed,
})
}
pub fn intersect(&self, other: &Self) -> Result<Self> {
self.check_compatibility(other)?;
let new_theta = self.theta.min(other.theta);
let mut new_entries = HashSet::with_capacity(self.k);
for &hash in &self.entries {
if hash < new_theta && other.entries.contains(&hash) {
new_entries.insert(hash);
}
}
Ok(Self {
lg_k: self.lg_k,
k: self.k,
entries: new_entries,
theta: new_theta,
seed: self.seed,
})
}
pub fn difference(&self, other: &Self) -> Result<Self> {
self.check_compatibility(other)?;
let new_theta = self.theta.min(other.theta);
let mut new_entries = HashSet::with_capacity(self.k);
for &hash in &self.entries {
if hash < new_theta && !other.entries.contains(&hash) {
new_entries.insert(hash);
}
}
Ok(Self {
lg_k: self.lg_k,
k: self.k,
entries: new_entries,
theta: new_theta,
seed: self.seed,
})
}
fn check_compatibility(&self, other: &Self) -> Result<()> {
if self.lg_k != other.lg_k {
return Err(SketchError::IncompatibleSketches {
reason: format!("lg_k mismatch: {} vs {}", self.lg_k, other.lg_k),
});
}
if self.seed != other.seed {
return Err(SketchError::IncompatibleSketches {
reason: format!("seed mismatch: {} vs {}", self.seed, other.seed),
});
}
Ok(())
}
fn hash_item<T: Hash>(&self, item: &T) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
self.seed.hash(&mut hasher);
item.hash(&mut hasher);
hasher.finish()
}
fn rebuild_with_lower_theta(&mut self) {
let mut sorted_entries: Vec<u64> = self.entries.iter().copied().collect();
sorted_entries.sort_unstable();
if sorted_entries.len() > self.k {
let new_theta = sorted_entries[self.k - 1];
self.entries.retain(|&hash| hash < new_theta);
self.theta = new_theta;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_creation() {
let sketch = ThetaSketch::new(12).unwrap();
assert_eq!(sketch.lg_k, 12);
assert_eq!(sketch.k, 4096);
assert_eq!(sketch.theta, u64::MAX);
assert!(sketch.is_empty());
}
#[test]
fn test_hash_consistency() {
let sketch = ThetaSketch::new(12).unwrap();
let hash1 = sketch.hash_item(&"test");
let hash2 = sketch.hash_item(&"test");
assert_eq!(hash1, hash2, "Hash should be deterministic");
}
#[test]
fn test_seed_affects_hash() {
let sketch1 = ThetaSketch::new(12).unwrap();
let sketch2 = ThetaSketch::with_seed(12, 1234).unwrap();
let hash1 = sketch1.hash_item(&"test");
let hash2 = sketch2.hash_item(&"test");
assert_ne!(
hash1, hash2,
"Different seeds should produce different hashes"
);
}
}