use crate::{HllError, HyperLogLog, alpha_m, fnv1a64};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone)]
pub struct SparseHyperLogLog {
p: u32,
m: u32,
alpha: f64,
sparse: Option<Vec<(u32, u8)>>,
dense: Option<HyperLogLog>,
threshold: usize,
}
impl core::fmt::Debug for SparseHyperLogLog {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SparseHyperLogLog")
.field("p", &self.p)
.field("sparse", &self.is_sparse())
.field("entries", &self.entry_count())
.field("estimate", &self.estimate())
.finish()
}
}
impl SparseHyperLogLog {
pub fn new(precision: u32) -> Self {
let p = precision.clamp(crate::MIN_PRECISION, crate::MAX_PRECISION);
let m = 1u32 << p;
let threshold = (m / 4) as usize;
Self::with_threshold(p, threshold)
}
pub fn with_threshold(precision: u32, threshold: usize) -> Self {
let p = precision.clamp(crate::MIN_PRECISION, crate::MAX_PRECISION);
let m = 1u32 << p;
let alpha = alpha_m(m);
Self {
p,
m,
alpha,
sparse: Some(Vec::new()),
dense: None,
threshold: threshold.max(1),
}
}
pub(crate) fn from_dense(dense: HyperLogLog) -> Self {
let p = dense.precision();
let m = dense.register_count();
Self {
p,
m,
alpha: alpha_m(m),
sparse: None,
dense: Some(dense),
threshold: (m / 4) as usize,
}
}
pub fn precision(&self) -> u32 {
self.p
}
pub fn register_count(&self) -> u32 {
self.m
}
pub fn is_sparse(&self) -> bool {
self.sparse.is_some()
}
pub fn threshold(&self) -> usize {
self.threshold
}
pub fn standard_error(&self) -> f64 {
crate::RSE_CONSTANT / (self.m as f64).sqrt()
}
pub fn state_bytes(&self) -> usize {
match (&self.sparse, &self.dense) {
(Some(list), _) => list.len() * 5,
(None, Some(d)) => d.state_bytes(),
_ => 0,
}
}
pub fn is_empty(&self) -> bool {
match (&self.sparse, &self.dense) {
(Some(list), _) => list.is_empty(),
(None, Some(d)) => d.is_empty(),
_ => true,
}
}
pub fn clear(&mut self) {
self.sparse = Some(Vec::new());
self.dense = None;
}
pub fn entry_count(&self) -> usize {
if let Some(list) = &self.sparse {
list.len()
} else if let Some(d) = &self.dense {
d.registers().iter().filter(|&&r| r != 0).count()
} else {
0
}
}
pub fn add(&mut self, key: &str) -> bool {
self.add_bytes(key.as_bytes())
}
pub fn add_u64(&mut self, key: u64) -> bool {
self.add_bytes(&key.to_be_bytes())
}
pub fn add_bytes(&mut self, key: &[u8]) -> bool {
let h = fnv1a64(key);
let idx = (h >> (64 - self.p)) as u32;
let w = (h << self.p) | (1u64 << (self.p - 1));
let r = (w.leading_zeros() + 1) as u8;
if let Some(list) = self.sparse.as_mut() {
if let Some(pos) = list.iter().position(|(i, _)| *i == idx) {
if r > list[pos].1 {
list[pos].1 = r;
return true;
}
false
} else {
list.push((idx, r));
if list.len() >= self.threshold {
self.promote();
}
true
}
} else if let Some(d) = self.dense.as_mut() {
d.add_bytes(key)
} else {
false
}
}
pub fn estimate(&self) -> f64 {
if let Some(list) = &self.sparse {
let m = self.m as f64;
let held_sum: f64 = list.iter().map(|(_, r)| 2f64.powi(-(*r as i32))).sum();
let zero_count = self.m as usize - list.len();
let sum = held_sum + zero_count as f64;
let raw = self.alpha * m * m / sum;
if zero_count > 0 && raw <= 2.5 * m {
-m * (zero_count as f64 / m).ln()
} else {
raw
}
} else if let Some(d) = &self.dense {
d.estimate()
} else {
0.0
}
}
pub fn merge(&mut self, other: &Self) -> Result<(), HllError> {
if self.p != other.p {
return Err(HllError::PrecisionMismatch {
left: self.p,
right: other.p,
});
}
if other.dense.is_some() {
self.promote();
}
if let Some(list) = self.sparse.as_mut() {
let entries = other.sparse.as_ref().expect("other is sparse here");
for &(idx, r) in entries {
match list.iter().position(|(i, _)| *i == idx) {
Some(pos) => {
if r > list[pos].1 {
list[pos].1 = r;
}
}
None => list.push((idx, r)),
}
}
if list.len() >= self.threshold {
self.promote();
}
return Ok(());
}
let target = self.dense.as_mut().expect("promoted above");
match &other.dense {
Some(d) => target.merge(d),
None => {
let entries = other.sparse.as_ref().expect("sparse when not dense");
target.apply_sparse(entries);
Ok(())
}
}
}
pub fn promote(&mut self) {
if self.dense.is_some() {
return;
}
let list = self.sparse.take().unwrap_or_default();
let mut dense = HyperLogLog::new(self.p);
dense.apply_sparse(&list);
self.dense = Some(dense);
}
pub fn as_dense(&self) -> Option<&HyperLogLog> {
self.dense.as_ref()
}
pub fn to_dense(&self) -> HyperLogLog {
match &self.dense {
Some(d) => {
let mut out = HyperLogLog::new(self.p);
let _ = out.merge(d);
out
}
None => {
let mut out = HyperLogLog::new(self.p);
if let Some(list) = &self.sparse {
out.apply_sparse(list);
}
out
}
}
}
pub(crate) fn entries(&self) -> Option<&[(u32, u8)]> {
self.sparse.as_deref()
}
}
impl HyperLogLog {
pub(crate) fn apply_sparse(&mut self, list: &[(u32, u8)]) {
for &(idx, r) in list {
let i = idx as usize;
if i < self.registers.len() && r > self.registers[i] {
self.registers[i] = r;
}
}
}
}
mod wire {
use super::SparseHyperLogLog;
use crate::HllError;
use crate::codec::{ENC_DENSE, ENC_SPARSE, HEADER_LEN, read_header, read_u32, write_header};
impl SparseHyperLogLog {
pub fn to_bytes(&self) -> Vec<u8> {
if let Some(d) = self.as_dense() {
return d.to_bytes();
}
let entries = self.entries().unwrap_or(&[]);
let mut out = Vec::with_capacity(HEADER_LEN + 8 + entries.len() * 5);
write_header(&mut out, ENC_SPARSE, self.precision());
out.extend_from_slice(&(self.threshold() as u32).to_be_bytes());
out.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for &(idx, r) in entries {
out.extend_from_slice(&idx.to_be_bytes());
out.push(r);
}
out
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, HllError> {
let (encoding, p) = read_header(bytes)?;
if encoding == ENC_DENSE {
return Ok(Self::from_dense(crate::HyperLogLog::from_bytes(bytes)?));
}
if encoding != ENC_SPARSE {
return Err(HllError::UnsupportedEncoding(encoding));
}
if bytes.len() < HEADER_LEN + 8 {
return Err(HllError::Truncated {
expected: HEADER_LEN + 8,
actual: bytes.len(),
});
}
let threshold = read_u32(bytes, HEADER_LEN) as usize;
let count = read_u32(bytes, HEADER_LEN + 4) as usize;
let expected = HEADER_LEN + 8 + count * 5;
if bytes.len() < expected {
return Err(HllError::Truncated {
expected,
actual: bytes.len(),
});
}
let mut out = Self::with_threshold(p, threshold);
let list = out.sparse.as_mut().expect("fresh sketch is sparse");
for i in 0..count {
let at = HEADER_LEN + 8 + i * 5;
list.push((read_u32(bytes, at), bytes[at + 4]));
}
if list.len() >= out.threshold {
out.promote();
}
Ok(out)
}
}
}
#[cfg(test)]
#[path = "sparse_tests.rs"]
mod tests;