use rand::{Rng, XorShiftRng};
use siphasher::sip::SipHasher;
use std::cmp;
use std::f64;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
pub struct HyperLogLog<T> {
alpha: f64,
p: usize,
registers: Vec<u8>,
hasher: SipHasher,
_marker: PhantomData<T>,
}
impl<T> HyperLogLog<T>
where
T: Hash,
{
fn get_hasher() -> SipHasher {
let mut rng = XorShiftRng::new_unseeded();
SipHasher::new_with_keys(rng.next_u64(), rng.next_u64())
}
fn get_alpha(p: usize) -> f64 {
assert!(4 <= p && p <= 16);
match p {
4 => 0.673,
5 => 0.697,
6 => 0.709,
p => 0.7213 / (1.0 + 1.079 / f64::from(1 << p)),
}
}
pub fn new(error_probability: f64) -> Self {
assert!(0.0 < error_probability && error_probability < 1.0);
let p = (1.04 / error_probability).powi(2).ln().ceil() as usize;
let alpha = Self::get_alpha(p);
let registers_len = 1 << p;
HyperLogLog {
alpha,
p,
registers: vec![0; registers_len],
hasher: Self::get_hasher(),
_marker: PhantomData,
}
}
pub fn insert(&mut self, item: &T) {
let mut sip = self.hasher;
item.hash(&mut sip);
let hash = sip.finish();
let register_index = hash as usize & (self.registers.len() - 1);
let value = (!hash >> self.p).trailing_zeros() as u8;
self.registers[register_index] = cmp::max(self.registers[register_index], value + 1);
}
pub fn merge(&mut self, other: &HyperLogLog<T>) {
assert_eq!(self.p, other.p);
for (index, value) in self.registers.iter_mut().enumerate() {
*value = cmp::max(*value, other.registers[index]);
}
}
fn get_estimate(&self) -> f64 {
let len = self.registers.len() as f64;
1.0 / (self.alpha * len * len * self.registers.iter().map(|value| 1.0 / 2.0f64.powi(i32::from(*value))).sum::<f64>())
}
pub fn len(&self) -> f64 {
let len = self.registers.len() as f64;
match self.get_estimate() {
x if x <= 2.5 * len => {
let zeros = self.registers
.iter()
.map(|value| if *value == 0 { 1 } else { 0 })
.sum::<u64>();
len * (len / zeros as f64).ln()
},
x if x <= 1.0 / 3.0 * 2.0f64.powi(32) => x,
x => -(2.0f64.powi(32)) * (1.0 - x / 2.0f64.powi(32)).ln(),
}
}
pub fn is_empty(&self) -> bool {
self.len() < f64::EPSILON
}
pub fn clear(&mut self) {
for value in &mut self.registers {
*value = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::HyperLogLog;
use std::f64::EPSILON;
#[test]
#[should_panic]
fn test_panic_new_invalid_error_probability() {
let _hhl: HyperLogLog<u32> = HyperLogLog::new(0.0);
}
#[test]
#[should_panic]
fn test_panic_new_mismatch_error_iprobability() {
let mut hhl1: HyperLogLog<u32> = HyperLogLog::new(0.1);
let hhl2: HyperLogLog<u32> = HyperLogLog::new(0.2);
hhl1.merge(&hhl2);
}
#[test]
fn test_simple() {
let mut hhl = HyperLogLog::new(0.01);
assert!(hhl.is_empty());
assert!(hhl.len() < EPSILON);
for key in &[0, 1, 2, 0, 1, 2] {
hhl.insert(&key);
}
assert!(!hhl.is_empty());
assert!((hhl.len().round() - 3.0).abs() < EPSILON);
hhl.clear();
assert!(hhl.is_empty());
}
#[test]
fn test_merge() {
let mut hhl1 = HyperLogLog::new(0.01);
for key in &[0, 1, 2, 0, 1, 2] {
hhl1.insert(&key);
}
let mut hhl2 = HyperLogLog::new(0.01);
for key in &[0, 1, 3, 0, 1, 3] {
hhl2.insert(&key);
}
assert!((hhl1.len().round() - 3.0).abs() < EPSILON);
assert!((hhl2.len().round() - 3.0).abs() < EPSILON);
hhl1.merge(&hhl2);
assert!((hhl1.len().round() - 4.0).abs() < EPSILON);
}
}