use crate::common::SketchError;
use xxhash_rust::xxh64::xxh64;
#[derive(Clone, Debug)]
pub struct CountingBloomFilter {
counters: Vec<u8>,
k: usize,
m: usize,
n: usize,
count: usize,
has_overflow: bool,
}
impl CountingBloomFilter {
const MAX_COUNT: u8 = 15;
pub fn new(n: usize, fpr: f64) -> Self {
assert!(n > 0, "Expected number of elements must be > 0");
assert!(
fpr > 0.0 && fpr < 1.0,
"False positive rate must be in (0, 1)"
);
let m = (-(n as f64) * fpr.ln() / (std::f64::consts::LN_2.powi(2))).ceil() as usize;
let k = ((m as f64 / n as f64) * std::f64::consts::LN_2).ceil() as usize;
let k = k.max(1);
Self::with_params(n, m, k)
}
pub fn with_params(n: usize, m: usize, k: usize) -> Self {
assert!(n > 0, "Expected number of elements must be > 0");
assert!(m > 0, "Number of counters must be > 0");
assert!(k > 0, "Number of hash functions must be > 0");
let num_bytes = m.div_ceil(2);
CountingBloomFilter {
counters: vec![0u8; num_bytes],
k,
m,
n,
count: 0,
has_overflow: false,
}
}
pub fn num_counters(&self) -> usize {
self.m
}
pub fn num_hash_functions(&self) -> usize {
self.k
}
pub fn capacity(&self) -> usize {
self.n
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn has_overflow(&self) -> bool {
self.has_overflow
}
pub fn insert(&mut self, key: &[u8]) {
for i in 0..self.k {
let idx = self.hash(key, i);
self.increment_counter(idx);
}
self.count += 1;
}
pub fn remove(&mut self, key: &[u8]) -> bool {
if !self.contains(key) {
return false;
}
for i in 0..self.k {
let idx = self.hash(key, i);
self.decrement_counter(idx);
}
if self.count > 0 {
self.count -= 1;
}
true
}
pub fn contains(&self, key: &[u8]) -> bool {
for i in 0..self.k {
let idx = self.hash(key, i);
if self.get_counter(idx) == 0 {
return false;
}
}
true
}
pub fn count_estimate(&self, key: &[u8]) -> u8 {
let mut min_count = Self::MAX_COUNT;
for i in 0..self.k {
let idx = self.hash(key, i);
let count = self.get_counter(idx);
min_count = min_count.min(count);
}
min_count
}
#[inline]
fn get_counter(&self, idx: usize) -> u8 {
let byte_idx = idx / 2;
if idx.is_multiple_of(2) {
self.counters[byte_idx] & 0x0F
} else {
(self.counters[byte_idx] >> 4) & 0x0F
}
}
#[inline]
fn increment_counter(&mut self, idx: usize) {
let byte_idx = idx / 2;
let current = self.get_counter(idx);
if current < Self::MAX_COUNT {
if idx.is_multiple_of(2) {
self.counters[byte_idx] = (self.counters[byte_idx] & 0xF0) | (current + 1);
} else {
self.counters[byte_idx] = (self.counters[byte_idx] & 0x0F) | ((current + 1) << 4);
}
} else {
self.has_overflow = true;
}
}
#[inline]
fn decrement_counter(&mut self, idx: usize) {
let byte_idx = idx / 2;
let current = self.get_counter(idx);
if current > 0 {
if idx.is_multiple_of(2) {
self.counters[byte_idx] = (self.counters[byte_idx] & 0xF0) | (current - 1);
} else {
self.counters[byte_idx] = (self.counters[byte_idx] & 0x0F) | ((current - 1) << 4);
}
}
}
#[inline]
fn hash(&self, key: &[u8], i: usize) -> usize {
let h1 = xxh64(key, 0) as usize;
let h2 = xxh64(key, 0x9E3779B9) as usize;
(h1.wrapping_add(i.wrapping_mul(h2))) % self.m
}
pub fn clear(&mut self) {
self.counters.fill(0);
self.count = 0;
self.has_overflow = false;
}
pub fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.m != other.m || self.k != other.k {
return Err(SketchError::IncompatibleSketches {
reason: "Counting Bloom filters have different parameters".to_string(),
});
}
for i in 0..self.m {
let self_count = self.get_counter(i);
let other_count = other.get_counter(i);
let new_count =
(self_count as u16 + other_count as u16).min(Self::MAX_COUNT as u16) as u8;
let byte_idx = i / 2;
if i % 2 == 0 {
self.counters[byte_idx] = (self.counters[byte_idx] & 0xF0) | new_count;
} else {
self.counters[byte_idx] = (self.counters[byte_idx] & 0x0F) | (new_count << 4);
}
}
self.count += other.count;
self.has_overflow = self.has_overflow || other.has_overflow;
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(24 + self.counters.len());
bytes.extend_from_slice(&(self.m as u64).to_le_bytes());
bytes.extend_from_slice(&(self.k as u64).to_le_bytes());
bytes.extend_from_slice(&(self.n as u64).to_le_bytes());
bytes.extend_from_slice(&self.counters);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 24 {
return Err(SketchError::DeserializationError(
"Insufficient data for CountingBloomFilter header".to_string(),
));
}
let m = u64::from_le_bytes(bytes[0..8].try_into().unwrap()) as usize;
let k = u64::from_le_bytes(bytes[8..16].try_into().unwrap()) as usize;
let n = u64::from_le_bytes(bytes[16..24].try_into().unwrap()) as usize;
let expected_len = 24 + m.div_ceil(2);
if bytes.len() < expected_len {
return Err(SketchError::DeserializationError(format!(
"Expected {} bytes, got {}",
expected_len,
bytes.len()
)));
}
let counters = bytes[24..expected_len].to_vec();
let mut count = 0;
for i in 0..m {
let byte_idx = i / 2;
let val = if i % 2 == 0 {
counters[byte_idx] & 0x0F
} else {
(counters[byte_idx] >> 4) & 0x0F
};
if val > 0 {
count += 1;
}
}
count /= k.max(1);
Ok(CountingBloomFilter {
counters,
k,
m,
n,
count,
has_overflow: false,
})
}
pub fn false_positive_rate(&self) -> f64 {
let fill_ratio = 1.0 - (-((self.count * self.k) as f64) / self.m as f64).exp();
fill_ratio.powi(self.k as i32)
}
pub fn memory_usage(&self) -> usize {
self.counters.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let filter = CountingBloomFilter::new(1000, 0.01);
assert!(filter.is_empty());
assert!(!filter.has_overflow());
}
#[test]
fn test_insert_contains() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"hello");
assert!(filter.contains(b"hello"));
assert!(!filter.is_empty());
}
#[test]
fn test_remove() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"hello");
assert!(filter.contains(b"hello"));
assert!(filter.remove(b"hello"));
assert!(!filter.contains(b"hello"));
}
#[test]
fn test_multiple_inserts() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"key1");
filter.insert(b"key2");
filter.insert(b"key3");
assert!(filter.contains(b"key1"));
assert!(filter.contains(b"key2"));
assert!(filter.contains(b"key3"));
assert!(!filter.contains(b"key4"));
}
#[test]
fn test_remove_maintains_others() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"key1");
filter.insert(b"key2");
filter.remove(b"key1");
assert!(!filter.contains(b"key1"));
assert!(filter.contains(b"key2"));
}
#[test]
fn test_count_estimate() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"hello");
filter.insert(b"hello");
filter.insert(b"hello");
let count = filter.count_estimate(b"hello");
assert!(count >= 1, "Count should be at least 1");
}
#[test]
fn test_serialization() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"key1");
filter.insert(b"key2");
let bytes = filter.to_bytes();
let restored = CountingBloomFilter::from_bytes(&bytes).unwrap();
assert!(restored.contains(b"key1"));
assert!(restored.contains(b"key2"));
assert!(!restored.contains(b"key3"));
}
#[test]
fn test_clear() {
let mut filter = CountingBloomFilter::new(100, 0.01);
filter.insert(b"hello");
assert!(!filter.is_empty());
filter.clear();
assert!(filter.is_empty());
assert!(!filter.contains(b"hello"));
}
}