use std::{
collections::HashMap,
sync::{
Mutex, PoisonError,
atomic::{AtomicU64, Ordering},
},
};
use super::component::Hash32;
pub trait FrequencyStore: Send + Sync {
fn record(&self, value: Hash32);
fn count(&self, value: Hash32) -> u64;
fn total(&self) -> u64;
}
#[derive(Debug, Default)]
pub struct FrequencyTable {
counts: Mutex<Counts>,
max_values: Option<usize>,
dropped: AtomicU64,
}
#[derive(Debug, Default)]
struct Counts {
per_value: HashMap<Hash32, u64>,
total: u64,
}
impl FrequencyTable {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(max_frequency_values: Option<usize>) -> Self {
Self {
counts: Mutex::new(Counts::default()),
max_values: max_frequency_values,
dropped: AtomicU64::new(0),
}
}
pub fn record(&self, value: Hash32) {
let mut counts = self.lock();
if let Some(existing) = counts.per_value.get_mut(&value) {
*existing += 1;
counts.total += 1;
return;
}
if let Some(cap) = self.max_values
&& counts.per_value.len() >= cap
{
drop(counts);
self.dropped.fetch_add(1, Ordering::Relaxed);
return;
}
counts.per_value.insert(value, 1);
counts.total += 1;
}
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub fn count(&self, value: Hash32) -> u64 {
self.lock().per_value.get(&value).copied().unwrap_or(0)
}
pub fn total(&self) -> u64 {
self.lock().total
}
#[allow(clippy::cast_precision_loss)] pub fn u_estimate(&self, value: Hash32) -> f64 {
let counts = self.lock();
if counts.total == 0 {
return 0.0;
}
let hits = counts.per_value.get(&value).copied().unwrap_or(0);
hits as f64 / counts.total as f64
}
fn lock(&self) -> std::sync::MutexGuard<'_, Counts> {
self.counts.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl FrequencyStore for FrequencyTable {
fn record(&self, value: Hash32) {
FrequencyTable::record(self, value);
}
fn count(&self, value: Hash32) -> u64 {
FrequencyTable::count(self, value)
}
fn total(&self) -> u64 {
FrequencyTable::total(self)
}
}
#[cfg(test)]
mod tests {
use super::FrequencyTable;
use crate::fuzzy::component::Salt;
#[test]
fn counts_and_total_track_sightings() {
let salt = Salt::random();
let table = FrequencyTable::new();
let common = salt.hash("Chrome/Windows");
let rare = salt.hash("Lynx/Plan9");
table.record(common);
table.record(common);
table.record(common);
table.record(rare);
assert_eq!(table.count(common), 3);
assert_eq!(table.count(rare), 1);
assert_eq!(table.total(), 4);
}
#[test]
fn rare_values_have_lower_u_than_common() {
let salt = Salt::random();
let table = FrequencyTable::new();
let common = salt.hash("common");
let rare = salt.hash("rare");
for _ in 0..9 {
table.record(common);
}
table.record(rare);
assert!(table.u_estimate(rare) < table.u_estimate(common));
assert!((table.u_estimate(rare) - 0.1).abs() < 1e-9);
}
#[test]
fn distinct_value_cap_drops_overflow_but_keeps_counting_tracked() {
let salt = Salt::random();
let table = FrequencyTable::with_capacity(Some(2));
let a = salt.hash("a");
let b = salt.hash("b");
let c = salt.hash("c");
table.record(a);
table.record(b);
table.record(c); table.record(c); table.record(a);
assert_eq!(table.count(a), 2);
assert_eq!(table.count(b), 1);
assert_eq!(table.count(c), 0); assert_eq!(table.dropped(), 2);
assert_eq!(table.total(), 3);
}
#[test]
fn cold_table_returns_zero() {
let table = FrequencyTable::new();
assert!(table.u_estimate(Salt::random().hash("x")).abs() < f64::EPSILON);
}
}