Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Libraries ought to never panic but instead gracefully return errors. #6

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ bitvec = "0.17.4"
twox-hash = "1.5.0"

[dev-dependencies]
criterion = "0.3.2"
criterion = "0.3.3"
rand = "0.7.3"

[[bench]]
Expand Down
51 changes: 31 additions & 20 deletions src/bloom_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//! - [Bloom Filters by Example](https://llimllib.github.io/bloomfilter-tutorial/)
//! - [Bloom Filter Calculator](https://hur.st/bloomfilter/)

use crate::error::CreationError;
use bitvec::bitvec;
use std::f64::consts::{E, LN_2};
use std::hash::{BuildHasher, Hash, Hasher};
Expand All @@ -34,13 +35,17 @@ const LN2_SQUARED: f64 = LN_2 * LN_2;
///
/// # Example
/// ```rust
/// # use flit::error::CreationError;
/// # pub fn main() -> Result<(), CreationError> {
/// use flit::BloomFilter;
///
/// let mut filter = BloomFilter::new(0.01, 10000);
/// let mut filter = BloomFilter::new(0.01, 10000)?;
/// filter.add(&"Hello, world!");
///
/// assert_eq!(filter.might_contain(&"Hello, world!"), true); // probably true
/// assert_eq!(filter.might_contain(&"Dogs are cool!"), false); // definitely false!
/// # Ok(())
/// # }
/// ```
pub struct BloomFilter<T> {
n: u64,
Expand All @@ -58,34 +63,34 @@ impl<T: Hash> BloomFilter<T> {
/// The parameters influence the size of the filter, as well as the number of
/// hashes that must be applied to the items.
///
/// # Panics
/// # Error Results
///
/// This function will panic if `false_positive_rate` is not between 0 and 1 (non inclusive),
/// This function will return `Err` if `false_positive_rate` is not between 0 and 1 (non inclusive),
/// or if `estimated_items` is not greater than 0.
pub fn new(false_positive_rate: f64, estimated_items: usize) -> Self {
assert!(
false_positive_rate > 0_f64 && false_positive_rate < 1_f64,
"False positive rate must be between 0 and 1 (non-inclusive)"
);
assert!(
estimated_items > 0,
"Number of estimated items must be greater than zero"
);
pub fn new(false_positive_rate: f64, estimated_items: usize) -> Result<Self, CreationError> {
if false_positive_rate < 0_f64 || false_positive_rate > 1_f64 {
return Err(CreationError::InvalidFalsePositiveRange(
false_positive_rate,
));
}
if estimated_items <= 0 {
return Err(CreationError::InvalidEstimatedItems(estimated_items));
}

let num_bits = -(estimated_items as f64) * false_positive_rate.ln() / LN2_SQUARED;
let num_hashes = (num_bits / estimated_items as f64) * LN_2;

let num_bits = num_bits.ceil() as u64;
let num_hashes = num_hashes.ceil() as u32;

BloomFilter {
Ok(BloomFilter {
n: 0,
m: num_bits,
k: num_hashes,
bit_vec: bitvec![0; num_bits as usize],
build_hasher: RandomXxHashBuilder::default(),
_phantom: PhantomData,
}
})
}

/// Adds the `item` to the filter by setting the appropriate bits in the filter to `true`.
Expand Down Expand Up @@ -148,29 +153,35 @@ mod tests {
use super::*;

#[test]
fn test_num_bits_and_hashes() {
let filter = BloomFilter::<&str>::new(0.01_f64, 216553);
fn test_num_bits_and_hashes() -> Result<(), CreationError> {
let filter = BloomFilter::<&str>::new(0.01_f64, 216553)?;

assert_eq!(filter.m, 2_075_674);
assert_eq!(filter.k, 7);

Ok(())
}

#[test]
fn test_false_positive_rate_empty() {
let filter = BloomFilter::<&str>::new(0.01_f64, 216553);
fn test_false_positive_rate_empty() -> Result<(), CreationError> {
let filter = BloomFilter::<&str>::new(0.01_f64, 216553)?;

// False positive rate with nothing added to the filter should be 0.
assert_eq!(filter.false_positive_rate(), 0_f64);

Ok(())
}

#[test]
fn test_add() {
let mut filter = BloomFilter::new(0.03_f64, 10);
fn test_add() -> Result<(), CreationError> {
let mut filter = BloomFilter::new(0.03_f64, 10)?;

filter.add(&"Hello, world!");

assert!(filter.false_positive_rate() > 0.0);
assert_eq!(filter.might_contain(&"Hello, world!"), true);
assert_eq!(filter.might_contain(&"Dogs are cool!"), false);

Ok(())
}
}
16 changes: 16 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// Possible errors returned when attempting to create `BloomFilter` with faulty arguments.
#[derive(Debug)]
pub enum CreationError {
/// False positive rate must be between 0 and 1 (non-inclusive).
InvalidFalsePositiveRange(f64),
/// Number of estimated items must be greater than zero.
InvalidEstimatedItems(usize),
}

impl std::fmt::Display for CreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{:?}", self)
}
}

impl std::error::Error for CreationError {}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
//! [`BloomFilter`]: bloom_filter/struct.BloomFilter.html
pub mod bloom_filter;
pub use bloom_filter::BloomFilter;
pub mod error;