use crate::CuckooFilter;
const DEFAULT_INITIAL_BUCKETS_HINT: usize = 1024;
const DEFAULT_GROW_THRESHOLD: f64 = 0.95;
const GROWTH_FACTOR: usize = 2;
const BUCKET_SIZE: usize = 4;
pub struct DynamicCuckooFilter {
layers: Vec<CuckooFilter>,
layer_capacities: Vec<usize>,
grow_threshold: f64,
}
impl DynamicCuckooFilter {
pub fn new(initial_capacity: usize) -> Self {
Self::with_threshold(initial_capacity, DEFAULT_GROW_THRESHOLD)
}
pub fn with_threshold(initial_capacity: usize, grow_threshold: f64) -> Self {
let cap = initial_capacity.max(DEFAULT_INITIAL_BUCKETS_HINT / BUCKET_SIZE);
let t = if grow_threshold.is_finite() && grow_threshold > 0.0 && grow_threshold < 1.0 {
grow_threshold
} else {
DEFAULT_GROW_THRESHOLD
};
Self {
layers: vec![CuckooFilter::with_capacity(cap)],
layer_capacities: vec![cap],
grow_threshold: t,
}
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn len(&self) -> usize {
self.layers.iter().map(|l| l.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn insert(&mut self, key: &str) -> bool {
if self.should_grow() {
self.grow();
}
let active = self.layers.len() - 1;
if self.layers[active].insert(key) {
return true;
}
self.grow();
let active = self.layers.len() - 1;
self.layers[active].insert(key)
}
pub fn contains(&self, key: &str) -> bool {
self.layers.iter().any(|l| l.contains(key))
}
pub fn delete(&mut self, key: &str) -> bool {
for layer in self.layers.iter_mut().rev() {
if layer.delete(key) {
return true;
}
}
false
}
pub fn load_factor(&self) -> f64 {
let active = self.layers.len() - 1;
let cap = self.layer_capacities[active];
if cap == 0 {
0.0
} else {
self.layers[active].len() as f64 / cap as f64
}
}
fn should_grow(&self) -> bool {
let active = self.layers.len() - 1;
let cap = self.layer_capacities[active];
if cap == 0 {
return false;
}
self.layers[active].len() as f64 / cap as f64 >= self.grow_threshold
}
fn grow(&mut self) {
let last = self.layer_capacities.len() - 1;
let new_cap = self.layer_capacities[last] * GROWTH_FACTOR;
self.layers.push(CuckooFilter::with_capacity(new_cap));
self.layer_capacities.push(new_cap);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_below_threshold() {
let mut d = DynamicCuckooFilter::new(2000);
for i in 0..500u32 {
assert!(d.insert(&format!("k{i}")));
}
for i in 0..500u32 {
assert!(d.contains(&format!("k{i}")));
}
assert_eq!(d.layer_count(), 1, "no grow expected below threshold");
}
#[test]
fn grows_when_threshold_crossed() {
let mut d = DynamicCuckooFilter::with_threshold(256, 0.5);
for i in 0..1000u32 {
assert!(d.insert(&format!("k{i}")));
}
assert!(
d.layer_count() >= 2,
"expected growth, got {} layers",
d.layer_count()
);
for i in 0..1000u32 {
assert!(d.contains(&format!("k{i}")), "lost k{i}");
}
}
#[test]
fn delete_walks_layers_newest_first() {
let mut d = DynamicCuckooFilter::with_threshold(64, 0.25);
for i in 0..200u32 {
d.insert(&format!("k{i}"));
}
assert!(d.layer_count() >= 2);
for i in 0..200u32 {
assert!(d.delete(&format!("k{i}")), "could not delete k{i}");
}
assert_eq!(d.len(), 0);
}
#[test]
fn delete_unknown_returns_false() {
let mut d = DynamicCuckooFilter::new(100);
d.insert("known");
assert!(!d.delete("never-inserted"));
assert!(d.contains("known"));
}
#[test]
fn empty_filter_rejects_anything() {
let d = DynamicCuckooFilter::new(100);
assert!(!d.contains("any"));
assert!(d.is_empty());
}
#[test]
fn load_factor_resets_after_grow() {
let mut d = DynamicCuckooFilter::with_threshold(64, 0.4);
for i in 0..200u32 {
d.insert(&format!("k{i}"));
}
assert!(d.load_factor() < 1.0);
}
#[test]
fn cumulative_len_tracks_all_layers() {
let mut d = DynamicCuckooFilter::with_threshold(64, 0.3);
let n = 500;
for i in 0..n {
d.insert(&format!("k{i}"));
}
assert_eq!(d.len(), n as usize);
assert!(d.layer_count() >= 2);
}
#[test]
fn invalid_threshold_falls_back_to_default() {
let d = DynamicCuckooFilter::with_threshold(100, f64::NAN);
assert!((d.grow_threshold - 0.95).abs() < 1e-12);
let d2 = DynamicCuckooFilter::with_threshold(100, 1.5);
assert!((d2.grow_threshold - 0.95).abs() < 1e-12);
}
}