use crate::error::Result;
use crate::filters::standard::StandardBloomFilter;
use crate::hash::{BloomHasher, StdHasher};
use std::hash::Hash;
mod state {
pub struct Initial;
pub struct WithItems {
pub(super) expected_items: usize,
}
pub struct Complete {
pub(super) expected_items: usize,
pub(super) fp_rate: f64,
}
}
use state::{Complete, Initial, WithItems};
pub struct StandardBloomFilterBuilder<State, H = StdHasher> {
state: State,
hasher: H,
}
impl StandardBloomFilterBuilder<Initial, StdHasher> {
#[must_use]
pub fn new() -> Self {
Self {
state: Initial,
hasher: StdHasher::new(),
}
}
}
impl Default for StandardBloomFilterBuilder<Initial, StdHasher> {
fn default() -> Self {
Self::new()
}
}
impl<H: BloomHasher + Clone> StandardBloomFilterBuilder<Initial, H> {
#[must_use]
pub fn expected_items(self, n: usize) -> StandardBloomFilterBuilder<WithItems, H> {
StandardBloomFilterBuilder {
state: WithItems { expected_items: n },
hasher: self.hasher,
}
}
#[must_use]
pub fn hasher<H2: BloomHasher + Clone>(
self,
hasher: H2,
) -> StandardBloomFilterBuilder<Initial, H2> {
StandardBloomFilterBuilder {
state: Initial,
hasher,
}
}
}
impl<H: BloomHasher + Clone> StandardBloomFilterBuilder<WithItems, H> {
#[must_use]
pub fn false_positive_rate(self, p: f64) -> StandardBloomFilterBuilder<Complete, H> {
StandardBloomFilterBuilder {
state: Complete {
expected_items: self.state.expected_items,
fp_rate: p,
},
hasher: self.hasher,
}
}
#[must_use]
pub fn hasher<H2: BloomHasher + Clone>(
self,
hasher: H2,
) -> StandardBloomFilterBuilder<WithItems, H2> {
StandardBloomFilterBuilder {
state: WithItems {
expected_items: self.state.expected_items,
},
hasher,
}
}
}
impl<H: BloomHasher + Clone> StandardBloomFilterBuilder<Complete, H> {
#[must_use]
pub fn hasher<H2: BloomHasher + Clone>(
self,
hasher: H2,
) -> StandardBloomFilterBuilder<Complete, H2> {
StandardBloomFilterBuilder {
state: Complete {
expected_items: self.state.expected_items,
fp_rate: self.state.fp_rate,
},
hasher,
}
}
pub fn build<T: Hash>(self) -> Result<StandardBloomFilter<T, H>> {
StandardBloomFilter::with_hasher(self.state.expected_items, self.state.fp_rate, self.hasher)
}
pub fn build_with_metadata<T: Hash>(
self,
) -> Result<(StandardBloomFilter<T, H>, FilterMetadata)> {
let n = self.state.expected_items;
let p = self.state.fp_rate;
let filter = StandardBloomFilter::with_hasher(n, p, self.hasher)?;
let meta = FilterMetadata {
expected_items: n,
fp_rate: p,
filter_size: filter.size(),
num_hashes: filter.hash_count(),
bytes_per_item: filter.size() as f64 / 8.0 / n as f64,
};
Ok((filter, meta))
}
}
#[derive(Debug, Clone)]
pub struct FilterMetadata {
pub expected_items: usize,
pub fp_rate: f64,
pub filter_size: usize,
pub num_hashes: usize,
pub bytes_per_item: f64,
}
impl FilterMetadata {
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.filter_size.div_ceil(64) * 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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filters::StandardBloomFilter;
use crate::hash::StdHasher;
#[test]
fn minimal_build() {
let f: StandardBloomFilter<u64> = StandardBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build()
.unwrap();
assert!(f.is_empty());
assert_eq!(f.expected_items(), 10_000);
assert!((f.target_fpr() - 0.01).abs() < f64::EPSILON);
}
#[test]
fn seeded_hasher_injection() {
let f: StandardBloomFilter<u64, StdHasher> = StandardBloomFilterBuilder::new()
.hasher(StdHasher::with_seed(0xCAFE))
.expected_items(1_000)
.false_positive_rate(0.01)
.build()
.unwrap();
f.insert(&42u64);
assert!(f.contains(&42u64));
}
#[test]
fn hasher_set_between_items_and_fpr() {
let f: StandardBloomFilter<u64, StdHasher> = StandardBloomFilterBuilder::new()
.expected_items(1_000)
.hasher(StdHasher::with_seed(99))
.false_positive_rate(0.01)
.build()
.unwrap();
assert!(f.is_empty());
}
#[test]
fn hasher_set_after_fpr() {
let f: StandardBloomFilter<u64, StdHasher> = StandardBloomFilterBuilder::new()
.expected_items(1_000)
.false_positive_rate(0.01)
.hasher(StdHasher::with_seed(7))
.build()
.unwrap();
assert!(f.is_empty());
}
#[test]
fn build_with_metadata_consistent_with_filter() {
let (f, meta): (StandardBloomFilter<u64>, _) = StandardBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build_with_metadata()
.unwrap();
assert_eq!(f.size(), meta.filter_size);
assert_eq!(f.hash_count(), meta.num_hashes);
assert_eq!(meta.expected_items, 10_000);
assert!((meta.fp_rate - 0.01).abs() < f64::EPSILON);
assert!(meta.bytes_per_item > 0.0);
}
#[test]
fn metadata_memory_units_consistent() {
let (f, meta): (StandardBloomFilter<u64>, _) = StandardBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build_with_metadata()
.unwrap();
assert_eq!(
meta.memory_bytes(),
meta.filter_size.div_ceil(64) * 8,
"memory_bytes() must equal the bit-array heap allocation exactly"
);
assert!(
meta.memory_bytes() <= f.memory_usage(),
"memory_bytes() must not exceed the filter's total memory_usage()"
);
assert!((meta.memory_kb() - meta.memory_bytes() as f64 / 1024.0).abs() < 1e-9);
assert!((meta.memory_mb() - meta.memory_kb() / 1024.0).abs() < 1e-9);
}
#[test]
fn zero_items_rejected() {
assert!(build(0, 0.01).is_err());
}
#[test]
fn invalid_fpr_zero() {
assert!(build(1_000, 0.0).is_err());
}
#[test]
fn invalid_fpr_one() {
assert!(build(1_000, 1.0).is_err());
}
#[test]
fn invalid_fpr_negative() {
assert!(build(1_000, -0.1).is_err());
}
#[test]
fn invalid_fpr_above_one() {
assert!(build(1_000, 1.5).is_err());
}
#[test]
fn no_false_negatives() {
let f: StandardBloomFilter<u64> = StandardBloomFilterBuilder::new()
.expected_items(10_000)
.false_positive_rate(0.01)
.build()
.unwrap();
for i in 0..1_000u64 {
f.insert(&i);
}
for i in 0..1_000u64 {
assert!(f.contains(&i), "false negative at {i}");
}
}
#[test]
fn large_filter_builds_successfully() {
let f: StandardBloomFilter<u64> = StandardBloomFilterBuilder::new()
.expected_items(1_000_000)
.false_positive_rate(0.001)
.build()
.unwrap();
assert!(f.size() > 0);
}
fn build(n: usize, p: f64) -> Result<StandardBloomFilter<u64>> {
StandardBloomFilterBuilder::new()
.expected_items(n)
.false_positive_rate(p)
.build()
}
}