use std::hash::{BuildHasher, Hash};
#[derive(Debug, Clone)]
pub struct HyperLogLog<S = std::hash::RandomState> {
precision: u8,
registers: Vec<u8>,
hasher_builder: S,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidPrecision;
impl std::fmt::Display for InvalidPrecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("HyperLogLog precision must be in 4..=18")
}
}
impl std::error::Error for InvalidPrecision {}
impl HyperLogLog<std::hash::RandomState> {
pub fn new(precision: u8) -> Result<Self, InvalidPrecision> {
Self::with_hasher(precision, std::hash::RandomState::new())
}
}
impl<S: BuildHasher> HyperLogLog<S> {
pub fn with_hasher(precision: u8, hasher_builder: S) -> Result<Self, InvalidPrecision> {
if !(4..=18).contains(&precision) {
return Err(InvalidPrecision);
}
let m = 1usize << precision;
Ok(Self {
precision,
registers: vec![0u8; m],
hasher_builder,
})
}
pub fn add(&mut self, item: impl Hash) {
let h = self.hasher_builder.hash_one(&item);
self.add_hash(h);
}
pub fn add_hash(&mut self, h: u64) {
let m_bits = self.precision as u32;
let idx = (h >> (64 - m_bits)) as usize;
let w = h.wrapping_shl(m_bits);
let lz = if w == 0 {
(64 - m_bits) + 1
} else {
w.leading_zeros() + 1
};
if lz as u8 > self.registers[idx] {
self.registers[idx] = lz as u8;
}
}
pub fn register_count(&self) -> usize {
self.registers.len()
}
pub fn precision(&self) -> u8 {
self.precision
}
pub fn count(&self) -> u64 {
let m = self.registers.len() as f64;
let mut sum = 0.0f64;
let mut zeros = 0usize;
for &r in &self.registers {
sum += 2.0f64.powi(-(r as i32));
if r == 0 {
zeros += 1;
}
}
let alpha = alpha_m(m);
let raw = alpha * m * m / sum;
if raw <= 2.5 * m && zeros > 0 {
let lc = m * (m / zeros as f64).ln();
return lc.round() as u64;
}
raw.round() as u64
}
pub fn clear(&mut self) {
for r in &mut self.registers {
*r = 0;
}
}
pub fn is_empty(&self) -> bool {
self.registers.iter().all(|&r| r == 0)
}
}
fn alpha_m(m: f64) -> f64 {
match m as usize {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / m),
}
}
impl<S: BuildHasher> crate::correlate::Mergeable for HyperLogLog<S> {
fn merge(&mut self, other: Self) {
assert_eq!(
self.precision, other.precision,
"HyperLogLog::merge requires matching precision",
);
for (a, b) in self.registers.iter_mut().zip(other.registers) {
if b > *a {
*a = b;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::correlate::Mergeable;
use std::hash::BuildHasherDefault;
type FixedHll = HyperLogLog<BuildHasherDefault<std::collections::hash_map::DefaultHasher>>;
fn new_fixed(p: u8) -> FixedHll {
HyperLogLog::with_hasher(p, BuildHasherDefault::default()).unwrap()
}
#[test]
fn precision_bounds_enforced() {
assert!(HyperLogLog::new(3).is_err());
assert!(HyperLogLog::new(19).is_err());
assert!(HyperLogLog::new(4).is_ok());
assert!(HyperLogLog::new(18).is_ok());
}
#[test]
fn empty_count_is_zero() {
let h = new_fixed(10);
assert_eq!(h.count(), 0);
assert!(h.is_empty());
}
#[test]
fn small_cardinality_within_linear_counting() {
let mut h = new_fixed(14);
for i in 0..100u64 {
h.add(i);
}
let c = h.count();
let err = ((c as i64 - 100).abs()) as f64 / 100.0;
assert!(
err < 0.05,
"expected ~100, got {c} ({:.1}% err)",
err * 100.0
);
}
#[test]
fn medium_cardinality_within_hll_error_bound() {
let mut h = new_fixed(14);
for i in 0..10_000u64 {
h.add(i);
}
let c = h.count();
let err = ((c as i64 - 10_000).abs()) as f64 / 10_000.0;
assert!(
err < 0.05,
"expected ~10000, got {c} ({:.1}% err)",
err * 100.0
);
}
#[test]
fn duplicates_dont_increase_count() {
let mut h = new_fixed(10);
for _ in 0..1000 {
h.add(42u64); }
assert_eq!(h.count(), 1);
}
#[test]
fn clear_resets() {
let mut h = new_fixed(10);
for i in 0..1000u64 {
h.add(i);
}
assert!(!h.is_empty());
h.clear();
assert!(h.is_empty());
assert_eq!(h.count(), 0);
}
#[test]
fn merge_equals_combined_add() {
let mut a = new_fixed(14);
let mut b = new_fixed(14);
let mut combined = new_fixed(14);
for i in 0..500u64 {
a.add(i);
combined.add(i);
}
for i in 500..1000u64 {
b.add(i);
combined.add(i);
}
a.merge(b);
assert_eq!(a.registers, combined.registers);
assert_eq!(a.count(), combined.count());
}
#[test]
fn merge_is_commutative() {
let mut a = new_fixed(12);
let mut b = new_fixed(12);
for i in 0..1000u64 {
a.add(i);
}
for i in 500..1500u64 {
b.add(i);
}
let mut ab = a.clone();
let mut ba = b.clone();
ab.merge(b);
ba.merge(a);
assert_eq!(ab.registers, ba.registers);
}
#[test]
#[should_panic(expected = "matching precision")]
fn merge_panics_on_precision_mismatch() {
let mut a = new_fixed(12);
let b = new_fixed(14);
a.merge(b);
}
#[test]
fn add_hash_bypass_works() {
let mut h = new_fixed(10);
h.add_hash(0xdead_beef_dead_beef);
h.add_hash(0xcafe_babe_cafe_babe);
h.add_hash(0xdead_beef_dead_beef); let c = h.count();
assert!(
(1..=4).contains(&c),
"expected ~2 distinct from add_hash, got {c}"
);
}
}