use crate::common::{validation, Mergeable, Sketch, SketchError};
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
pub struct HyperLogLog {
precision: u8,
registers: Vec<u8>,
}
impl HyperLogLog {
pub const MIN_PRECISION: u8 = 4;
pub const MAX_PRECISION: u8 = 18;
pub fn new(precision: u8) -> Result<Self, SketchError> {
if !(Self::MIN_PRECISION..=Self::MAX_PRECISION).contains(&precision) {
return Err(SketchError::InvalidParameter {
param: "precision".to_string(),
value: precision.to_string(),
constraint: format!(
"must be between {} and {}",
Self::MIN_PRECISION,
Self::MAX_PRECISION
),
});
}
let m = 1usize << precision;
let registers = vec![0u8; m];
Ok(HyperLogLog {
precision,
registers,
})
}
#[inline]
pub fn precision(&self) -> u8 {
self.precision
}
#[inline]
pub fn num_registers(&self) -> usize {
1 << self.precision
}
pub fn standard_error(&self) -> f64 {
1.04 / (self.num_registers() as f64).sqrt()
}
pub fn update<T: Hash>(&mut self, item: &T) {
let hash = self.hash_item(item);
self.update_hash(hash);
}
#[inline(always)]
fn hash_item<T: Hash>(&self, item: &T) -> u64 {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
hasher.finish()
}
#[inline]
pub fn update_hash(&mut self, hash: u64) {
let idx = (hash >> (64 - self.precision)) as usize;
let w = hash << self.precision | (1u64 << (self.precision - 1));
let rho = (w.leading_zeros() + 1) as u8;
if rho > self.registers[idx] {
self.registers[idx] = rho;
}
}
fn raw_estimate(&self) -> f64 {
let m = self.num_registers() as f64;
let sum: f64 = self
.registers
.iter()
.map(|&r| 2.0_f64.powi(-(r as i32)))
.sum();
let alpha_m = self.alpha();
alpha_m * m * m / sum
}
fn alpha(&self) -> f64 {
let m = self.num_registers() as f64;
match self.num_registers() {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / m),
}
}
fn count_zeros(&self) -> usize {
self.registers.iter().filter(|&&r| r == 0).count()
}
fn linear_counting(&self, zeros: usize) -> f64 {
let m = self.num_registers() as f64;
m * (m / zeros as f64).ln()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(1 + self.registers.len());
bytes.push(self.precision);
bytes.extend_from_slice(&self.registers);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
validation::validate_min_size(bytes.len(), 1)?;
validation::validate_byte_size(bytes.len())?;
let precision = bytes[0];
validation::validate_precision(precision)?;
let expected_len = 1 + (1usize << precision);
if bytes.len() != expected_len {
return Err(SketchError::DeserializationError(format!(
"Invalid serialization: expected {} bytes for precision {}, got {}",
expected_len,
precision,
bytes.len()
)));
}
let registers = bytes[1..].to_vec();
Ok(HyperLogLog {
precision,
registers,
})
}
pub fn from_redis_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 4 {
return Err(SketchError::DeserializationError(
"Invalid Redis HLL format: too short".to_string(),
));
}
if &bytes[0..4] != b"HYLL" {
return Err(SketchError::DeserializationError(
"Invalid Redis HLL format: missing HYLL header".to_string(),
));
}
let precision = 14u8;
let m = 1usize << precision;
if bytes.len() < 16 + m {
return Err(SketchError::DeserializationError(
"Invalid Redis HLL format: insufficient data for dense representation".to_string(),
));
}
let mut registers = vec![0u8; m];
let data = &bytes[16..];
for (i, reg) in registers.iter_mut().enumerate() {
let byte_idx = (i * 6) / 8;
let bit_offset = (i * 6) % 8;
if byte_idx + 1 < data.len() {
let val = if bit_offset <= 2 {
(data[byte_idx] >> bit_offset) & 0x3F
} else {
((data[byte_idx] >> bit_offset) | (data[byte_idx + 1] << (8 - bit_offset)))
& 0x3F
};
*reg = val;
}
}
Ok(HyperLogLog {
precision,
registers,
})
}
pub fn to_redis_bytes(&self) -> Vec<u8> {
let m = self.num_registers();
let mut bytes = Vec::with_capacity(16 + (m * 6).div_ceil(8));
bytes.extend_from_slice(b"HYLL");
bytes.push(1);
bytes.extend_from_slice(&[0u8; 11]);
let packed_len = (m * 6).div_ceil(8);
let mut packed = vec![0u8; packed_len];
for (i, ®) in self.registers.iter().enumerate() {
let val = reg.min(63); let bit_idx = i * 6;
let byte_idx = bit_idx / 8;
let bit_offset = bit_idx % 8;
packed[byte_idx] |= val << bit_offset;
if bit_offset > 2 && byte_idx + 1 < packed_len {
packed[byte_idx + 1] |= val >> (8 - bit_offset);
}
}
bytes.extend_from_slice(&packed);
bytes
}
pub fn registers(&self) -> &[u8] {
&self.registers
}
}
impl Sketch for HyperLogLog {
type Item = u64;
fn update(&mut self, item: &Self::Item) {
self.update(item);
}
fn estimate(&self) -> f64 {
let m = self.num_registers() as f64;
let raw = self.raw_estimate();
if raw <= 2.5 * m {
let zeros = self.count_zeros();
if zeros > 0 {
return self.linear_counting(zeros);
}
}
let two_pow_32 = (1u64 << 32) as f64;
if raw > two_pow_32 / 30.0 {
return -two_pow_32 * (1.0 - raw / two_pow_32).ln();
}
raw
}
fn is_empty(&self) -> bool {
self.registers.iter().all(|&r| r == 0)
}
fn serialize(&self) -> Vec<u8> {
self.to_bytes()
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
Self::from_bytes(bytes)
}
}
impl Mergeable for HyperLogLog {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.precision != other.precision {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"Precision mismatch: {} vs {}",
self.precision, other.precision
),
});
}
for (i, &other_reg) in other.registers.iter().enumerate() {
if other_reg > self.registers[i] {
self.registers[i] = other_reg;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_hyperloglog() {
let hll = HyperLogLog::new(12).unwrap();
assert!(hll.is_empty());
assert_eq!(hll.precision(), 12);
assert_eq!(hll.num_registers(), 4096);
}
#[test]
fn test_invalid_precision() {
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 test_update() {
let mut hll = HyperLogLog::new(12).unwrap();
hll.update(&"hello");
assert!(!hll.is_empty());
}
#[test]
fn test_estimate_small() {
let mut hll = HyperLogLog::new(12).unwrap();
for i in 0..100 {
hll.update(&i);
}
let estimate = hll.estimate();
assert!(
(estimate - 100.0).abs() < 20.0,
"Estimate {} too far from 100",
estimate
);
}
#[test]
fn test_estimate_medium() {
let mut hll = HyperLogLog::new(12).unwrap();
for i in 0..10_000 {
hll.update(&i);
}
let estimate = hll.estimate();
let error = (estimate - 10_000.0).abs() / 10_000.0;
assert!(error < 0.05, "Error {} too high", error);
}
#[test]
fn test_merge() {
let mut hll1 = HyperLogLog::new(12).unwrap();
let mut hll2 = HyperLogLog::new(12).unwrap();
for i in 0..1000 {
hll1.update(&i);
}
for i in 500..1500 {
hll2.update(&i);
}
hll1.merge(&hll2).unwrap();
let estimate = hll1.estimate();
let error = (estimate - 1500.0).abs() / 1500.0;
assert!(
error < 0.1,
"Merged estimate {} too far from 1500",
estimate
);
}
#[test]
fn test_merge_precision_mismatch() {
let mut hll1 = HyperLogLog::new(10).unwrap();
let hll2 = HyperLogLog::new(12).unwrap();
assert!(hll1.merge(&hll2).is_err());
}
#[test]
fn test_serialization() {
let mut hll = HyperLogLog::new(12).unwrap();
for i in 0..1000 {
hll.update(&i);
}
let bytes = hll.to_bytes();
let restored = HyperLogLog::from_bytes(&bytes).unwrap();
assert_eq!(hll.precision, restored.precision);
assert_eq!(hll.registers, restored.registers);
}
#[test]
fn test_standard_error() {
let hll = HyperLogLog::new(12).unwrap();
let se = hll.standard_error();
assert!((se - 0.01625).abs() < 0.001);
}
#[test]
fn test_idempotent_updates() {
let mut hll = HyperLogLog::new(12).unwrap();
for _ in 0..1000 {
hll.update(&"same_item");
}
let estimate = hll.estimate();
assert!(
estimate < 2.0,
"Duplicate updates should not increase count"
);
}
}