use crate::core::params;
use crate::error::Result;
use crate::filters::scalable::ScalableBloomFilter;
use crate::hash::{BloomHasher, StdHasher};
use std::marker::PhantomData;
pub struct Initial;
pub struct WithCapacity;
pub struct Complete;
const DEFAULT_GROWTH_FACTOR: f64 = 2.0;
const DEFAULT_TIGHTENING_RATIO: f64 = 0.85;
pub struct ScalableBloomFilterBuilder<State, H = StdHasher> {
initial_capacity: Option<usize>,
fp_rate: Option<f64>,
growth_factor: f64,
tightening_ratio: f64,
_state: PhantomData<State>,
hasher: H,
}
impl ScalableBloomFilterBuilder<Initial, StdHasher> {
#[must_use]
pub fn new() -> Self {
Self {
initial_capacity: None,
fp_rate: None,
growth_factor: DEFAULT_GROWTH_FACTOR,
tightening_ratio: DEFAULT_TIGHTENING_RATIO,
_state: PhantomData,
hasher: StdHasher::new(),
}
}
}
impl<H> ScalableBloomFilterBuilder<Initial, H> {
#[must_use]
pub fn initial_capacity(self, capacity: usize) -> ScalableBloomFilterBuilder<WithCapacity, H> {
ScalableBloomFilterBuilder {
initial_capacity: Some(capacity),
fp_rate: self.fp_rate,
growth_factor: self.growth_factor,
tightening_ratio: self.tightening_ratio,
_state: PhantomData,
hasher: self.hasher,
}
}
}
impl<H> ScalableBloomFilterBuilder<WithCapacity, H> {
#[must_use]
pub fn false_positive_rate(self, fp_rate: f64) -> ScalableBloomFilterBuilder<Complete, H> {
ScalableBloomFilterBuilder {
initial_capacity: self.initial_capacity,
fp_rate: Some(fp_rate),
growth_factor: self.growth_factor,
tightening_ratio: self.tightening_ratio,
_state: PhantomData,
hasher: self.hasher,
}
}
#[must_use]
pub fn growth_factor(mut self, factor: f64) -> Self {
self.growth_factor = factor;
self
}
#[must_use]
pub fn tightening_ratio(mut self, ratio: f64) -> Self {
self.tightening_ratio = ratio;
self
}
}
impl<H> ScalableBloomFilterBuilder<Complete, H> {
#[must_use]
pub fn growth_factor(mut self, factor: f64) -> Self {
self.growth_factor = factor;
self
}
#[must_use]
pub fn tightening_ratio(mut self, ratio: f64) -> Self {
self.tightening_ratio = ratio;
self
}
}
impl<H: BloomHasher + Default + Clone> ScalableBloomFilterBuilder<Complete, H> {
pub fn build<T: std::hash::Hash>(self) -> Result<ScalableBloomFilter<T, H>> {
let initial_capacity = self.initial_capacity.expect("capacity must be set");
let fp_rate = self.fp_rate.expect("fp_rate must be set");
super::validation::validate_items(initial_capacity)?;
super::validation::validate_fp_rate(fp_rate)?;
super::validation::validate_growth_factor(self.growth_factor)?;
super::validation::validate_tightening_ratio(self.tightening_ratio)?;
let filter_size = params::optimal_bit_count(initial_capacity, fp_rate)?;
let num_hashes = params::optimal_hash_count(filter_size, initial_capacity)?;
params::validate_params(filter_size, initial_capacity, num_hashes)?;
let growth = crate::filters::scalable::GrowthStrategy::Geometric(self.growth_factor);
ScalableBloomFilter::with_strategy_and_hasher(
initial_capacity,
fp_rate,
self.tightening_ratio,
growth,
self.hasher,
)
}
pub fn build_with_metadata<T: std::hash::Hash>(
self,
) -> Result<(ScalableBloomFilter<T, H>, ScalableFilterMetadata)> {
let initial_capacity = self.initial_capacity.expect("capacity must be set");
let fp_rate = self.fp_rate.expect("fp_rate must be set");
super::validation::validate_items(initial_capacity)?;
super::validation::validate_fp_rate(fp_rate)?;
super::validation::validate_growth_factor(self.growth_factor)?;
super::validation::validate_tightening_ratio(self.tightening_ratio)?;
let filter_size = params::optimal_bit_count(initial_capacity, fp_rate)?;
let num_hashes = params::optimal_hash_count(filter_size, initial_capacity)?;
params::validate_params(filter_size, initial_capacity, num_hashes)?;
let growth = crate::filters::scalable::GrowthStrategy::Geometric(self.growth_factor);
let filter = ScalableBloomFilter::with_strategy_and_hasher(
initial_capacity,
fp_rate,
self.tightening_ratio,
growth,
self.hasher,
)?;
let max_fp_rate_bound = fp_rate / (1.0 - self.tightening_ratio);
let metadata = ScalableFilterMetadata {
initial_capacity,
initial_fp_rate: fp_rate,
growth_factor: self.growth_factor,
tightening_ratio: self.tightening_ratio,
initial_filter_size: filter_size,
initial_num_hashes: num_hashes,
max_fp_rate_bound,
};
Ok((filter, metadata))
}
}
impl Default for ScalableBloomFilterBuilder<Initial, StdHasher> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ScalableFilterMetadata {
pub initial_capacity: usize,
pub initial_fp_rate: f64,
pub growth_factor: f64,
pub tightening_ratio: f64,
pub initial_filter_size: usize,
pub initial_num_hashes: usize,
pub max_fp_rate_bound: f64,
}
impl ScalableFilterMetadata {
#[must_use]
pub fn slice_capacity(&self, i: usize) -> usize {
const MAX_CAP: f64 = usize::MAX as f64;
let computed = self.initial_capacity as f64 * self.growth_factor.powi(i as i32);
if computed > MAX_CAP || !computed.is_finite() {
return usize::MAX;
}
computed as usize
}
#[must_use]
pub fn slice_fp_rate(&self, i: usize) -> f64 {
self.initial_fp_rate * self.tightening_ratio.powi(i as i32)
}
#[must_use]
pub fn total_capacity(&self, s: usize) -> usize {
let mut total = 0usize;
for i in 0..s {
total = total.saturating_add(self.slice_capacity(i));
}
total
}
#[must_use]
pub fn slices_for_capacity(&self, target_capacity: usize) -> usize {
let mut slices = 0;
let mut total = 0usize;
while total < target_capacity {
total = total.saturating_add(self.slice_capacity(slices));
slices += 1;
if slices > crate::filters::scalable::MAX_FILTERS {
break;
}
}
slices
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_minimal() {
let filter: ScalableBloomFilter<String> = ScalableBloomFilterBuilder::new()
.initial_capacity(1_000)
.false_positive_rate(0.01)
.build()
.unwrap();
assert!(filter.is_empty());
}
#[test]
fn test_builder_invalid_capacity() {
let result: Result<ScalableBloomFilter<String>> = ScalableBloomFilterBuilder::new()
.initial_capacity(0)
.false_positive_rate(0.01)
.build();
assert!(result.is_err());
}
}