use crate::common::SketchError;
use std::collections::HashMap;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorType {
NoFalsePositives,
NoFalseNegatives,
}
#[derive(Debug, Clone)]
pub struct FrequentItems<T: Hash + Eq + Clone> {
max_size: usize,
items: HashMap<T, u64>,
offset: u64,
}
impl<T: Hash + Eq + Clone> FrequentItems<T> {
pub fn new(max_size: usize) -> Result<Self, SketchError> {
if max_size < 2 {
return Err(SketchError::InvalidParameter {
param: "max_size".to_string(),
value: max_size.to_string(),
constraint: "must be >= 2".to_string(),
});
}
Ok(Self {
max_size,
items: HashMap::with_capacity(max_size),
offset: 0,
})
}
pub fn update(&mut self, item: T) {
self.update_by(item, 1);
}
pub fn update_by(&mut self, item: T, count: u64) {
if count == 0 {
return;
}
*self.items.entry(item).or_insert(0) += count;
if self.items.len() > self.max_size {
self.purge();
}
}
fn purge(&mut self) {
let min_count = *self.items.values().min().unwrap_or(&0);
if min_count == 0 {
return;
}
self.items.retain(|_, count| *count > min_count);
self.offset += min_count;
}
pub fn frequent_items(&self, _error_type: ErrorType) -> Vec<(T, u64, u64)> {
let mut result = Vec::with_capacity(self.items.len());
for (item, &count) in &self.items {
let lower = count;
let upper = count + self.offset;
result.push((item.clone(), lower, upper));
}
result.sort_by(|a, b| b.1.cmp(&a.1));
result
}
pub fn get_estimate(&self, item: &T) -> Option<(u64, u64)> {
self.items
.get(item)
.map(|&count| (count, count + self.offset))
}
pub fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.max_size != other.max_size {
return Err(SketchError::IncompatibleSketches {
reason: format!("max_size mismatch: {} vs {}", self.max_size, other.max_size),
});
}
for (item, &count) in &other.items {
*self.items.entry(item.clone()).or_insert(0) += count;
}
self.offset += other.offset;
while self.items.len() > self.max_size {
self.purge();
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn num_items(&self) -> usize {
self.items.len()
}
pub fn max_size(&self) -> usize {
self.max_size
}
pub fn offset(&self) -> u64 {
self.offset
}
}
impl<T: Hash + Eq + Clone> PartialEq for FrequentItems<T> {
fn eq(&self, other: &Self) -> bool {
self.max_size == other.max_size && self.items == other.items && self.offset == other.offset
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
let mut sketch = FrequentItems::new(10).unwrap();
assert!(sketch.is_empty());
sketch.update("apple".to_string());
assert!(!sketch.is_empty());
assert_eq!(sketch.num_items(), 1);
}
#[test]
fn test_purge_mechanism() {
let mut sketch = FrequentItems::new(2).unwrap();
sketch.update("a".to_string());
sketch.update("b".to_string());
sketch.update("c".to_string());
assert!(sketch.num_items() <= 2);
assert!(sketch.offset() > 0);
}
#[test]
fn test_error_bounds() {
let mut sketch = FrequentItems::new(10).unwrap();
sketch.update_by("test".to_string(), 42);
let (lower, upper) = sketch.get_estimate(&"test".to_string()).unwrap();
assert!(lower <= 42);
assert!(upper >= 42);
}
}