use crate::core::params;
use crate::error::Result;
use crate::filters::counting::CountingBloomFilter;
use crate::filters::CounterSize;
use crate::hash::{BloomHasher, IndexingStrategy, StdHasher};
use std::marker::PhantomData;
pub struct Initial;
pub struct WithItems;
pub struct Complete;
pub struct CountingBloomFilterBuilder<State, H = StdHasher> {
expected_items: Option<usize>,
fp_rate: Option<f64>,
max_count: u16,
hash_strategy: IndexingStrategy,
_state: PhantomData<State>,
_hasher: PhantomData<H>,
}
impl CountingBloomFilterBuilder<Initial, StdHasher> {
#[must_use]
pub fn new() -> Self {
Self {
expected_items: None,
fp_rate: None,
max_count: 15,
hash_strategy: IndexingStrategy::EnhancedDouble,
_state: PhantomData,
_hasher: PhantomData,
}
}
}
impl<H> CountingBloomFilterBuilder<Initial, H> {
#[must_use]
pub fn expected_items(self, items: usize) -> CountingBloomFilterBuilder<WithItems, H> {
CountingBloomFilterBuilder {
expected_items: Some(items),
fp_rate: self.fp_rate,
max_count: self.max_count,
hash_strategy: self.hash_strategy,
_state: PhantomData,
_hasher: PhantomData,
}
}
}
impl<H> CountingBloomFilterBuilder<WithItems, H> {
#[must_use]
pub fn false_positive_rate(self, fp_rate: f64) -> CountingBloomFilterBuilder<Complete, H> {
CountingBloomFilterBuilder {
expected_items: self.expected_items,
fp_rate: Some(fp_rate),
max_count: self.max_count,
hash_strategy: self.hash_strategy,
_state: PhantomData,
_hasher: PhantomData,
}
}
#[must_use]
pub fn max_count(mut self, max_count: u16) -> Self {
self.max_count = max_count;
self
}
#[must_use]
pub fn hash_strategy(mut self, strategy: IndexingStrategy) -> Self {
self.hash_strategy = strategy;
self
}
}
impl<H> CountingBloomFilterBuilder<Complete, H> {
#[must_use]
pub fn max_count(mut self, max_count: u16) -> Self {
self.max_count = max_count;
self
}
#[must_use]
pub fn hash_strategy(mut self, strategy: IndexingStrategy) -> Self {
self.hash_strategy = strategy;
self
}
}
impl<H: BloomHasher + Default + Clone> CountingBloomFilterBuilder<Complete, H> {
fn compute_params(&self, expected_items: usize, fp_rate: f64) -> Result<(usize, usize)> {
let filter_size = params::optimal_bit_count(expected_items, fp_rate)?;
let num_hashes = params::optimal_hash_count(filter_size, expected_items)?;
params::validate_params(filter_size, expected_items, num_hashes)?;
Ok((filter_size, num_hashes))
}
pub fn build<T: std::hash::Hash + Send + Sync>(self) -> Result<CountingBloomFilter<T, H>> {
let expected_items = self.expected_items.expect("items must be set");
let fp_rate = self.fp_rate.expect("fp_rate must be set");
super::validation::validate_items(expected_items)?;
super::validation::validate_fp_rate(fp_rate)?;
super::validation::validate_max_count(self.max_count)?;
let (filter_size, num_hashes) = self.compute_params(expected_items, fp_rate)?;
let filter = CountingBloomFilter::with_full_params(
filter_size,
num_hashes,
CounterSize::from_max_count(self.max_count),
expected_items,
fp_rate,
);
Ok(filter)
}
pub fn build_with_metadata<T: std::hash::Hash + Send + Sync>(
self,
) -> Result<(CountingBloomFilter<T, H>, CountingFilterMetadata)> {
let expected_items = self.expected_items.expect("items must be set");
let fp_rate = self.fp_rate.expect("fp_rate must be set");
super::validation::validate_items(expected_items)?;
super::validation::validate_fp_rate(fp_rate)?;
super::validation::validate_max_count(self.max_count)?;
let (filter_size, num_hashes) = self.compute_params(expected_items, fp_rate)?;
let counter_size = CounterSize::from_max_count(self.max_count);
let filter = CountingBloomFilter::with_full_params(
filter_size,
num_hashes,
counter_size,
expected_items,
fp_rate,
);
let counter_bits = counter_size.bits() as u8;
let metadata = CountingFilterMetadata {
expected_items,
fp_rate,
filter_size,
num_hashes,
max_count: self.max_count,
counter_bits,
hash_strategy: self.hash_strategy,
memory_overhead_factor: counter_bits as f64,
};
Ok((filter, metadata))
}
}
impl Default for CountingBloomFilterBuilder<Initial, StdHasher> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CountingFilterMetadata {
pub expected_items: usize,
pub fp_rate: f64,
pub filter_size: usize,
pub num_hashes: usize,
pub max_count: u16,
pub counter_bits: u8,
pub hash_strategy: IndexingStrategy,
pub memory_overhead_factor: f64,
}
impl CountingFilterMetadata {
#[must_use]
pub fn memory_bytes(&self) -> usize {
(self.filter_size * self.counter_bits as usize).div_ceil(8)
}
#[must_use]
pub fn memory_kb(&self) -> f64 {
self.memory_bytes() as f64 / 1024.0
}
#[must_use]
pub fn memory_mb(&self) -> f64 {
self.memory_bytes() as f64 / (1024.0 * 1024.0)
}
#[must_use]
pub fn bytes_per_item(&self) -> f64 {
self.memory_bytes() as f64 / self.expected_items as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::IndexingStrategy;
#[test]
fn test_builder_minimal() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_builder_with_max_count() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.max_count(255)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_builder_with_strategy() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.hash_strategy(IndexingStrategy::Triple)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_builder_full_config() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.max_count(255)
.hash_strategy(IndexingStrategy::Double)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_builder_with_metadata() {
let (filter, metadata): (CountingBloomFilter<String>, _) =
CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build_with_metadata()
.unwrap();
assert!(filter.is_empty());
assert_eq!(metadata.expected_items, 10_000);
assert!((metadata.fp_rate - 0.01).abs() < 0.001);
assert!(metadata.filter_size > 0);
assert!(metadata.num_hashes > 0);
assert_eq!(metadata.max_count, 15);
assert_eq!(metadata.counter_bits, 4);
assert!((metadata.memory_overhead_factor - 4.0).abs() < 0.01);
}
#[test]
fn test_builder_8bit_counters() {
let (_, metadata): (CountingBloomFilter<String>, _) = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.max_count(255)
.build_with_metadata()
.unwrap();
assert_eq!(metadata.max_count, 255);
assert_eq!(metadata.counter_bits, 8);
assert!((metadata.memory_overhead_factor - 8.0).abs() < 0.01);
}
#[test]
fn test_builder_invalid_items() {
let result: Result<CountingBloomFilter<String>> = CountingBloomFilterBuilder::new()
.expected_items(0)
.false_positive_rate(0.01)
.build();
assert!(result.is_err());
}
#[test]
fn test_builder_invalid_fp_rate() {
let result: Result<CountingBloomFilter<String>> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.0)
.build();
assert!(result.is_err());
}
#[test]
fn test_builder_invalid_max_count() {
let result: Result<CountingBloomFilter<String>> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.max_count(0)
.build();
assert!(result.is_err());
}
#[test]
fn test_filter_insert_delete() {
let mut filter: CountingBloomFilter<&str> = CountingBloomFilterBuilder::new()
.expected_items(1_000)
.false_positive_rate(0.01)
.build()
.unwrap();
filter.insert(&"hello");
filter.insert(&"world");
assert!(filter.contains(&"hello"));
assert!(filter.contains(&"world"));
filter.delete(&"hello");
assert!(!filter.contains(&"hello"));
assert!(filter.contains(&"world"));
}
#[test]
fn test_filter_insert_delete_integers() {
let mut filter: CountingBloomFilter<i32> = CountingBloomFilterBuilder::new()
.expected_items(1_000)
.false_positive_rate(0.01)
.build()
.unwrap();
filter.insert(&12345);
filter.insert(&67890);
assert!(filter.contains(&12345));
assert!(filter.contains(&67890));
filter.delete(&12345);
assert!(!filter.contains(&12345));
assert!(filter.contains(&67890));
}
#[test]
fn test_filter_duplicate_insertions() {
let mut filter: CountingBloomFilter<&str> = CountingBloomFilterBuilder::new()
.expected_items(1_000)
.false_positive_rate(0.01)
.max_count(15)
.build()
.unwrap();
for _ in 0..10 {
filter.insert(&"test");
}
assert!(filter.contains(&"test"));
for _ in 0..10 {
filter.delete(&"test");
}
assert!(!filter.contains(&"test"));
}
#[test]
fn test_metadata_memory_calculations() {
let (_, metadata): (CountingBloomFilter<String>, _) = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build_with_metadata()
.unwrap();
let bytes = metadata.memory_bytes();
let kb = metadata.memory_kb();
let mb = metadata.memory_mb();
let bpi = metadata.bytes_per_item();
assert!(bytes > 0);
assert!(kb > 0.0);
assert!(mb > 0.0);
assert!(bpi > 0.0);
assert!((kb - bytes as f64 / 1024.0).abs() < 0.01);
assert!((mb - kb / 1024.0).abs() < 0.0001);
}
#[test]
fn test_different_strategies() {
let strategies = [
IndexingStrategy::Double,
IndexingStrategy::EnhancedDouble,
IndexingStrategy::Triple,
];
for strategy in strategies {
let mut filter: CountingBloomFilter<&str> = CountingBloomFilterBuilder::new()
.expected_items(1_000)
.false_positive_rate(0.01)
.hash_strategy(strategy)
.build()
.unwrap();
filter.insert(&"test");
assert!(filter.contains(&"test"));
filter.delete(&"test");
assert!(!filter.contains(&"test"));
}
}
#[test]
fn test_max_count_before_fp_rate() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.max_count(100)
.false_positive_rate(0.01)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_max_count_after_fp_rate() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.max_count(100)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_large_filter() {
let filter: CountingBloomFilter<String> = CountingBloomFilterBuilder::new()
.expected_items(1_000_000)
.false_positive_rate(0.001)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_counter_overflow_protection() {
let mut filter: CountingBloomFilter<&str> = CountingBloomFilterBuilder::new()
.expected_items(100)
.false_positive_rate(0.01)
.max_count(15)
.build()
.unwrap();
for _ in 0..15 {
filter.insert(&"overflow_test");
}
}
}