#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
use crate::error::{BloomCraftError, Result};
use std::f64::consts::LN_2;
const LN2_SQUARED: f64 = LN_2 * LN_2;
pub const MIN_FILTER_SIZE: usize = 8;
pub const MAX_HASH_FUNCTIONS: usize = 32;
pub const MIN_HASH_FUNCTIONS: usize = 1;
const MAX_LOAD_FACTOR: f64 = 2.0;
pub fn optimal_bit_count(n: usize, fp_rate: f64) -> Result<usize> {
if n == 0 {
return Err(BloomCraftError::invalid_item_count(n));
}
if fp_rate <= 0.0 || fp_rate >= 1.0 {
return Err(BloomCraftError::fp_rate_out_of_bounds(fp_rate));
}
if fp_rate >= 0.9999 {
return Err(BloomCraftError::fp_rate_out_of_bounds(fp_rate));
}
let n_f64 = n as f64;
let numerator = -n_f64 * fp_rate.ln();
let m = numerator / LN2_SQUARED;
if m > usize::MAX as f64 {
return Err(BloomCraftError::invalid_parameters(format!(
"Calculated filter size {:.0} exceeds system limits (usize::MAX = {})",
m,
usize::MAX
)));
}
let m_ceil = m.ceil();
let m_final = m_ceil.max(MIN_FILTER_SIZE as f64) as usize;
if m_final > usize::MAX / 2 {
return Err(BloomCraftError::invalid_parameters(format!(
"Calculated filter size {} exceeds reasonable bounds. \
Consider increasing false positive rate or reducing item count.",
m_final
)));
}
Ok(m_final)
}
pub fn optimal_hash_count(m: usize, n: usize) -> Result<usize> {
if m == 0 {
return Err(BloomCraftError::invalid_filter_size(m));
}
if n == 0 {
return Err(BloomCraftError::invalid_item_count(n));
}
let m_f64 = m as f64;
let n_f64 = n as f64;
let k = (m_f64 / n_f64) * LN_2;
let k_rounded = k.round() as usize;
let k_final = k_rounded.clamp(MIN_HASH_FUNCTIONS, MAX_HASH_FUNCTIONS);
Ok(k_final)
}
pub fn expected_fp_rate(m: usize, n: usize, k: usize) -> Result<f64> {
if m == 0 {
return Err(BloomCraftError::invalid_filter_size(m));
}
if !(MIN_HASH_FUNCTIONS..=MAX_HASH_FUNCTIONS).contains(&k) {
return Err(BloomCraftError::invalid_hash_count(
k,
MIN_HASH_FUNCTIONS,
MAX_HASH_FUNCTIONS,
));
}
if n == 0 {
return Ok(0.0);
}
let m_f64 = m as f64;
let n_f64 = n as f64;
let k_f64 = k as f64;
let exponent = -(k_f64 * n_f64) / m_f64;
let prob_bit_zero = exponent.exp();
let prob_bit_one = 1.0 - prob_bit_zero;
let fp_rate = prob_bit_one.powf(k_f64);
Ok(fp_rate.clamp(0.0, 1.0))
}
pub fn calculate_filter_params(n: usize, fp_rate: f64) -> Result<(usize, usize)> {
let m = optimal_bit_count(n, fp_rate)?;
let k = optimal_hash_count(m, n)?;
Ok((m, k))
}
pub fn validate_params(m: usize, n: usize, k: usize) -> Result<()> {
if m == 0 {
return Err(BloomCraftError::invalid_filter_size(m));
}
if n == 0 {
return Err(BloomCraftError::invalid_item_count(n));
}
if !(MIN_HASH_FUNCTIONS..=MAX_HASH_FUNCTIONS).contains(&k) {
return Err(BloomCraftError::invalid_hash_count(
k,
MIN_HASH_FUNCTIONS,
MAX_HASH_FUNCTIONS,
));
}
if m < k {
return Err(BloomCraftError::invalid_parameters(format!(
"Filter size ({} bits) must be at least as large as hash count ({}).",
m, k
)));
}
let load_factor = n as f64 / m as f64;
if load_factor > MAX_LOAD_FACTOR {
return Err(BloomCraftError::invalid_parameters(format!(
"Load factor {:.2} exceeds maximum {:.1}. Filter would have unacceptably high \
false positive rate (>50%). Increase filter size or reduce item count.",
load_factor, MAX_LOAD_FACTOR
)));
}
Ok(())
}
pub fn bits_per_element(fp_rate: f64) -> Result<f64> {
if fp_rate <= 0.0 || fp_rate >= 1.0 {
return Err(BloomCraftError::fp_rate_out_of_bounds(fp_rate));
}
let bpe = -fp_rate.ln() / LN2_SQUARED;
Ok(bpe)
}
#[inline]
pub fn optimal_k(n: usize, m: usize) -> Result<usize> {
optimal_hash_count(m, n)
}
#[inline]
pub fn optimal_m(n: usize, fp_rate: f64) -> Result<usize> {
optimal_bit_count(n, fp_rate)
}
#[cfg(test)]
mod tests {
use super::*;
const EXPECTED_BITS_1000_1PCT: usize = 9585; const EXPECTED_BITS_1000_0_1PCT: usize = 14377; const EXPECTED_HASH_9585_1000: usize = 7;
#[test]
fn test_ln2_squared_constant() {
let expected = 0.480_453_013_918_201_4;
assert!(
(LN2_SQUARED - expected).abs() < 1e-10,
"LN2_SQUARED constant incorrect: expected {}, got {}",
expected,
LN2_SQUARED
);
}
#[test]
fn test_optimal_bit_count_1_percent() {
let m = optimal_bit_count(1000, 0.01).unwrap();
assert!(
m >= EXPECTED_BITS_1000_1PCT && m <= EXPECTED_BITS_1000_1PCT + 1,
"Expected ~{}, got {}",
EXPECTED_BITS_1000_1PCT,
m
);
}
#[test]
fn test_optimal_bit_count_0_1_percent() {
let m = optimal_bit_count(1000, 0.001).unwrap();
assert!(
m >= EXPECTED_BITS_1000_0_1PCT && m <= EXPECTED_BITS_1000_0_1PCT + 1,
"Expected ~{}, got {}",
EXPECTED_BITS_1000_0_1PCT,
m
);
}
#[test]
fn test_optimal_bit_count_large_n() {
let m = optimal_bit_count(1_000_000, 0.01).unwrap();
assert!(m >= 9_585_000 && m <= 9_586_000);
}
#[test]
fn test_optimal_bit_count_zero_items_error() {
let result = optimal_bit_count(0, 0.01);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
BloomCraftError::InvalidItemCount { count: 0 }
));
}
#[test]
fn test_optimal_bit_count_invalid_fp_rate_zero() {
let result = optimal_bit_count(1000, 0.0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
BloomCraftError::FalsePositiveRateOutOfBounds { fp_rate } if fp_rate == 0.0
));
}
#[test]
fn test_optimal_bit_count_invalid_fp_rate_one() {
let result = optimal_bit_count(1000, 1.0);
assert!(result.is_err());
}
#[test]
fn test_optimal_bit_count_invalid_fp_rate_negative() {
let result = optimal_bit_count(1000, -0.1);
assert!(result.is_err());
}
#[test]
fn test_optimal_bit_count_invalid_fp_rate_greater_than_one() {
let result = optimal_bit_count(1000, 1.5);
assert!(result.is_err());
}
#[test]
fn test_optimal_hash_count_standard() {
let k = optimal_hash_count(9585, 1000).unwrap();
assert_eq!(
k, EXPECTED_HASH_9585_1000,
"Expected {} hash functions",
EXPECTED_HASH_9585_1000
);
}
#[test]
fn test_optimal_hash_count_clamping_max() {
let k = optimal_hash_count(100_000, 10).unwrap();
assert!(k <= MAX_HASH_FUNCTIONS);
}
#[test]
fn test_optimal_hash_count_clamping_min() {
let k = optimal_hash_count(10, 100_000).unwrap();
assert_eq!(k, MIN_HASH_FUNCTIONS);
}
#[test]
fn test_optimal_hash_count_zero_bits_error() {
let result = optimal_hash_count(0, 1000);
assert!(result.is_err());
}
#[test]
fn test_optimal_hash_count_zero_items_error() {
let result = optimal_hash_count(1000, 0);
assert!(result.is_err());
}
#[test]
fn test_expected_fp_rate_matches_target() {
let n = 1000;
let target_fp = 0.01;
let m = optimal_bit_count(n, target_fp).unwrap();
let k = optimal_hash_count(m, n).unwrap();
let actual_fp = expected_fp_rate(m, n, k).unwrap();
let error = (actual_fp - target_fp).abs() / target_fp;
assert!(
error < 0.1,
"FP rate error {:.2}% exceeds 10%. Expected {}, got {}",
error * 100.0,
target_fp,
actual_fp
);
}
#[test]
fn test_expected_fp_rate_empty_filter() {
let fp = expected_fp_rate(1000, 0, 7).unwrap();
assert_eq!(fp, 0.0, "Empty filter should have 0% FP rate");
}
#[test]
fn test_expected_fp_rate_full_filter() {
let fp = expected_fp_rate(1000, 1000, 7).unwrap();
assert!(fp > 0.5, "Saturated filter should have high FP rate");
}
#[test]
fn test_expected_fp_rate_invalid_zero_bits() {
let result = expected_fp_rate(0, 1000, 7);
assert!(result.is_err());
}
#[test]
fn test_expected_fp_rate_invalid_hash_count_zero() {
let result = expected_fp_rate(1000, 100, 0);
assert!(result.is_err());
}
#[test]
fn test_expected_fp_rate_invalid_hash_count_too_high() {
let result = expected_fp_rate(1000, 100, 100);
assert!(result.is_err());
}
#[test]
fn test_calculate_filter_params() {
let (m, k) = calculate_filter_params(1000, 0.01).unwrap();
assert!(m >= EXPECTED_BITS_1000_1PCT && m <= EXPECTED_BITS_1000_1PCT + 1);
assert_eq!(k, EXPECTED_HASH_9585_1000);
}
#[test]
fn test_calculate_filter_params_various_fp_rates() {
let test_cases = vec![
(1000, 0.1, 4792, 3),
(1000, 0.01, 9585, 7),
(1000, 0.001, 14377, 10),
];
for (n, fp, expected_m, expected_k) in test_cases {
let (m, k) = calculate_filter_params(n, fp).unwrap();
assert!(
m >= expected_m && m <= expected_m + 1,
"n={}, fp={}: expected m~{}, got {}",
n,
fp,
expected_m,
m
);
assert_eq!(
k, expected_k,
"n={}, fp={}: expected k={}, got {}",
n, fp, expected_k, k
);
}
}
#[test]
fn test_validate_params_valid() {
assert!(validate_params(1000, 100, 7).is_ok());
assert!(validate_params(10000, 1000, 10).is_ok());
}
#[test]
fn test_validate_params_zero_bits() {
let result = validate_params(0, 100, 7);
assert!(result.is_err());
}
#[test]
fn test_validate_params_zero_items() {
let result = validate_params(1000, 0, 7);
assert!(result.is_err());
}
#[test]
fn test_validate_params_invalid_hash_count() {
let result = validate_params(1000, 100, 0);
assert!(result.is_err());
let result = validate_params(1000, 100, 100);
assert!(result.is_err());
}
#[test]
fn test_validate_params_bits_less_than_hashes() {
let result = validate_params(5, 100, 10);
assert!(result.is_err());
}
#[test]
fn test_validate_params_high_load_factor() {
let result = validate_params(100, 250, 7);
assert!(result.is_err());
}
#[test]
fn test_validate_params_allows_moderate_saturation() {
assert!(validate_params(100, 150, 7).is_ok());
assert!(validate_params(100, 200, 7).is_ok());
}
#[test]
fn test_bits_per_element() {
const EXPECTED_BPE_1PCT: f64 = 9.6;
let bpe = bits_per_element(0.01).unwrap();
assert!(
(bpe - EXPECTED_BPE_1PCT).abs() < 0.1,
"Expected ~{} bpe, got {}",
EXPECTED_BPE_1PCT,
bpe
);
const EXPECTED_BPE_0_1PCT: f64 = 14.4; let bpe = bits_per_element(0.001).unwrap();
assert!(
(bpe - EXPECTED_BPE_0_1PCT).abs() < 0.1,
"Expected ~{} bpe, got {}",
EXPECTED_BPE_0_1PCT,
bpe
);
}
#[test]
fn test_bits_per_element_invalid_fp_rate() {
assert!(bits_per_element(0.0).is_err());
assert!(bits_per_element(1.0).is_err());
assert!(bits_per_element(-0.1).is_err());
assert!(bits_per_element(1.5).is_err());
}
#[test]
fn test_mathematical_consistency() {
let n = 1000;
let fp_rate = 0.01;
let m = optimal_bit_count(n, fp_rate).unwrap();
let bpe = bits_per_element(fp_rate).unwrap();
let expected_m = (n as f64 * bpe).ceil() as usize;
assert_eq!(
m, expected_m,
"optimal_bit_count and bits_per_element should be consistent"
);
}
#[test]
fn test_roundtrip_calculation() {
let n = 10000;
let target_fp = 0.005;
let (m, k) = calculate_filter_params(n, target_fp).unwrap();
let actual_fp = expected_fp_rate(m, n, k).unwrap();
assert!(
(actual_fp - target_fp).abs() / target_fp < 0.15,
"Roundtrip calculation: target {}, got {}",
target_fp,
actual_fp
);
}
#[test]
fn test_optimal_hash_count_various_ratios() {
let test_cases = vec![
(1000, 100, 7), (2000, 100, 14), (500, 100, 3), ];
for (m, n, expected_k) in test_cases {
let k = optimal_hash_count(m, n).unwrap();
assert_eq!(
k, expected_k,
"For m={}, n={}, expected k={}, got {}",
m, n, expected_k, k
);
}
}
#[test]
fn test_optimal_bit_count_various_fp_rates() {
let n = 1000;
let test_cases = vec![
(0.1, 4792), (0.01, 9585), (0.001, 14377), (0.0001, 19170), ];
for (fp_rate, expected_m) in test_cases {
let m = optimal_bit_count(n, fp_rate).unwrap();
assert!(
(m as i32 - expected_m).abs() <= 1,
"For fp_rate={}, expected ~{}, got {}",
fp_rate,
expected_m,
m
);
}
}
#[test]
fn test_validate_params_edge_cases() {
assert!(validate_params(1, 1, 1).is_ok());
assert!(validate_params(100, 1, 1).is_ok());
assert!(validate_params(5, 100, 10).is_err());
assert!(validate_params(100, 201, 7).is_err());
}
}