use core::hash::{Hash, Hasher};
const POW2_NEG_LUT: [f64; 65] = [
1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625, 0.001953125, 0.0009765625, 0.00048828125, 0.000244140625, 0.0001220703125, 6.103515625e-5, 3.0517578125e-5, 1.52587890625e-5, 7.62939453125e-6, 3.814697265625e-6, 1.9073486328125e-6, 9.5367431640625e-7, 4.76837158203125e-7, 2.384185791015625e-7, 1.1920928955078125e-7, 5.960464477539063e-8, 2.9802322387695312e-8, 1.4901161193847656e-8, 7.450580596923828e-9, 3.725290298461914e-9, 1.862645149230957e-9, 9.313225746154785e-10, 4.656612873077393e-10, 2.3283064365386963e-10, 1.1641532182693481e-10, 5.820766091346741e-11, 2.9103830456733704e-11, 1.4551915228366852e-11, 7.275957614183426e-12, 3.637978807091713e-12, 1.8189894035458565e-12, 9.094947017729282e-13, 4.547473508864641e-13, 2.2737367544323206e-13, 1.1368683772161603e-13, 5.684341886080802e-14, 2.842170943040401e-14, 1.4210854715202004e-14, 7.105427357601002e-15, 3.552713678800501e-15, 1.7763568394002505e-15, 8.881784197001252e-16, 4.440892098500626e-16, 2.220446049250313e-16, 1.1102230246251565e-16, 5.551115123125783e-17, 2.7755575615628914e-17, 1.3877787807814457e-17, 6.938893903907228e-18, 3.469446951953614e-18, 1.734723475976807e-18, 8.673617379884035e-19, 4.336808689942018e-19, 2.168404344971009e-19, 1.0842021724855044e-19, 5.421010862427522e-20, ];
#[inline]
#[must_use]
pub(crate) const fn splitmix64(x: u64) -> u64 {
let mut z = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
pub trait Mergeable {
fn merge(&mut self, other: &Self);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FnvHasher {
state: u64,
}
impl FnvHasher {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
state: Self::FNV_OFFSET,
}
}
#[inline]
const fn mix(mut h: u64) -> u64 {
h ^= h >> 33;
h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33;
h = h.wrapping_mul(0xc4ceb9fe1a85ec53);
h ^= h >> 33;
h
}
#[inline]
#[must_use]
pub fn hash_bytes(data: &[u8]) -> u64 {
let mut hasher = Self::new();
hasher.write(data);
Self::mix(hasher.state)
}
#[inline]
#[must_use]
pub fn hash_u64(value: u64) -> u64 {
Self::hash_bytes(&value.to_le_bytes())
}
#[inline]
#[must_use]
pub fn hash_u128(value: u128) -> u64 {
Self::hash_bytes(&value.to_le_bytes())
}
}
impl Default for FnvHasher {
fn default() -> Self {
Self::new()
}
}
impl Hasher for FnvHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.state ^= byte as u64;
self.state = self.state.wrapping_mul(Self::FNV_PRIME);
}
}
#[inline]
fn finish(&self) -> u64 {
Self::mix(self.state)
}
}
#[derive(Clone, Debug)]
pub struct HyperLogLogN<const M: usize> {
registers: [u8; M],
}
impl<const M: usize> HyperLogLogN<M> {
pub const M: usize = M;
pub const P: usize = {
assert!(
M.is_power_of_two(),
"HyperLogLogN<M>: M must be a power of two"
);
M.trailing_zeros() as usize
};
const ALPHA: f64 = 0.7213 / (1.0 + 1.079 / (M as f64));
#[inline]
pub const fn new() -> Self {
let _p: usize = Self::P;
Self {
registers: [0u8; M],
}
}
#[inline]
pub fn insert_hash(&mut self, hash: u64) {
let idx = (hash as usize) & (Self::M - 1);
let w = hash >> Self::P;
let rho = if w == 0 {
(64 - Self::P + 1) as u8
} else {
(w.leading_zeros() as usize - Self::P + 1) as u8
};
if rho > self.registers[idx] {
self.registers[idx] = rho;
}
}
#[inline]
pub fn insert<T: Hash>(&mut self, value: &T) {
let mut hasher = FnvHasher::new();
value.hash(&mut hasher);
self.insert_hash(hasher.finish());
}
#[inline]
pub fn insert_bytes(&mut self, bytes: &[u8]) {
self.insert_hash(FnvHasher::hash_bytes(bytes));
}
pub fn cardinality(&self) -> f64 {
let mut sum = 0.0f64;
let mut zeros = 0usize;
for ® in &self.registers {
let idx = (reg as usize).min(64);
sum += POW2_NEG_LUT[idx];
if reg == 0 {
zeros += 1;
}
}
let m = Self::M as f64;
let raw_estimate = Self::ALPHA * m * m / sum;
if raw_estimate <= 2.5 * m && zeros > 0 {
m * crate::det_math::ln64(m / zeros as f64)
} else {
raw_estimate
}
}
#[inline]
pub const fn registers(&self) -> &[u8] {
&self.registers
}
#[inline]
pub fn clear(&mut self) {
self.registers = [0u8; M];
}
}
impl<const M: usize> Default for HyperLogLogN<M> {
fn default() -> Self {
Self::new()
}
}
impl<const M: usize> Mergeable for HyperLogLogN<M> {
fn merge(&mut self, other: &Self) {
for (dst, &src) in self.registers.iter_mut().zip(other.registers.iter()) {
if src > *dst {
*dst = src;
}
}
}
}
pub type HyperLogLog10 = HyperLogLogN<1024>;
pub type HyperLogLog12 = HyperLogLogN<4096>;
pub type HyperLogLog14 = HyperLogLogN<16384>;
pub type HyperLogLog16 = HyperLogLogN<65536>;
pub type HyperLogLog = HyperLogLog14;
#[derive(Clone, Debug)]
pub struct DDSketchN<const BINS: usize> {
positive_bins: [u64; BINS],
negative_bins: [u64; BINS],
zero_count: u64,
count: u64,
min: f64,
max: f64,
sum: f64,
gamma: f64,
ln_gamma: f64,
alpha: f64,
offset: i32,
}
impl<const BINS: usize> DDSketchN<BINS> {
pub const BINS: usize = BINS;
pub fn new(alpha: f64) -> Self {
let gamma = (1.0 + alpha) / (1.0 - alpha);
let ln_gamma = crate::det_math::ln64(gamma);
let offset = (BINS / 4) as i32;
Self {
positive_bins: [0u64; BINS],
negative_bins: [0u64; BINS],
zero_count: 0,
count: 0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
sum: 0.0,
gamma,
ln_gamma,
alpha,
offset,
}
}
#[inline]
pub fn insert(&mut self, value: f64) {
if !value.is_finite() {
return;
}
self.count += 1;
self.sum += value;
if value < self.min {
self.min = value;
}
if value > self.max {
self.max = value;
}
if value > 0.0 {
self.add_at_key(false, self.bucket_key(value), 1);
} else if value < 0.0 {
self.add_at_key(true, self.bucket_key(-value), 1);
} else {
self.zero_count += 1;
}
}
#[inline]
fn bucket_key(&self, magnitude: f64) -> i64 {
(crate::det_math::ln64(magnitude) / self.ln_gamma).ceil() as i64
}
fn add_at_key(&mut self, negative: bool, key: i64, n: u64) {
let mut idx = key.saturating_add(i64::from(self.offset));
if idx < 0 && (self.positive_bins[BINS - 1] != 0 || self.negative_bins[BINS - 1] != 0) {
idx = 0;
} else if idx < 0 || idx >= BINS as i64 {
self.make_room(key);
idx = key
.saturating_add(i64::from(self.offset))
.clamp(0, BINS as i64 - 1);
}
let bins = if negative {
&mut self.negative_bins
} else {
&mut self.positive_bins
};
bins[idx as usize] = bins[idx as usize].saturating_add(n);
}
fn occupied(&self) -> Option<(usize, usize)> {
let used = |i: usize| self.positive_bins[i] != 0 || self.negative_bins[i] != 0;
let lo = (0..BINS).find(|&i| used(i))?;
let hi = (0..BINS).rev().find(|&i| used(i))?;
Some((lo, hi))
}
fn make_room(&mut self, key: i64) {
let bins = BINS as i64;
let (lo_key, hi_key) = match self.occupied() {
Some((lo, hi)) => {
let off = i64::from(self.offset);
(
(lo as i64).saturating_sub(off).min(key),
(hi as i64).saturating_sub(off).max(key),
)
}
None => (key, key),
};
let span = hi_key.saturating_sub(lo_key);
let new_offset = if span < bins {
((bins - 1 - span) / 2).saturating_sub(lo_key)
} else {
(bins - 1).saturating_sub(hi_key)
};
let shift = new_offset.saturating_sub(i64::from(self.offset));
for side in [&mut self.positive_bins, &mut self.negative_bins] {
let old = *side;
*side = [0u64; BINS];
for (i, &c) in old.iter().enumerate() {
if c != 0 {
let j = (i as i64).saturating_add(shift).clamp(0, bins - 1) as usize;
side[j] = side[j].saturating_add(c);
}
}
}
self.offset = new_offset.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
}
#[inline]
fn bucket_lower_bound(&self, idx: usize) -> f64 {
let exp = (idx as i64 - i64::from(self.offset)) as f64;
crate::det_math::powf64(self.gamma, exp - 1.0)
}
pub fn quantile(&self, q: f64) -> f64 {
if self.count == 0 {
return 0.0;
}
let rank = ((q * self.count as f64).ceil() as u64).max(1);
let mut cumulative = 0u64;
for (idx, &count) in self.negative_bins.iter().enumerate().rev() {
cumulative += count;
if cumulative >= rank {
return -self.bucket_lower_bound(idx);
}
}
cumulative += self.zero_count;
if cumulative >= rank {
return 0.0;
}
for (idx, &count) in self.positive_bins.iter().enumerate() {
cumulative += count;
if cumulative >= rank {
return self.bucket_lower_bound(idx);
}
}
self.max
}
#[inline]
pub const fn count(&self) -> u64 {
self.count
}
#[inline]
pub const fn sum(&self) -> f64 {
self.sum
}
#[inline]
pub fn mean(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.sum / self.count as f64
}
}
#[inline]
pub const fn min(&self) -> f64 {
self.min
}
#[inline]
pub const fn max(&self) -> f64 {
self.max
}
#[inline]
pub const fn alpha(&self) -> f64 {
self.alpha
}
pub fn clear(&mut self) {
self.positive_bins = [0u64; BINS];
self.negative_bins = [0u64; BINS];
self.zero_count = 0;
self.count = 0;
self.min = f64::INFINITY;
self.max = f64::NEG_INFINITY;
self.sum = 0.0;
self.offset = (BINS / 4) as i32;
}
}
impl<const BINS: usize> Mergeable for DDSketchN<BINS> {
fn merge(&mut self, other: &Self) {
let other_offset = i64::from(other.offset);
for i in 0..BINS {
let key = i as i64 - other_offset;
if other.positive_bins[i] != 0 {
self.add_at_key(false, key, other.positive_bins[i]);
}
if other.negative_bins[i] != 0 {
self.add_at_key(true, key, other.negative_bins[i]);
}
}
self.zero_count += other.zero_count;
self.count += other.count;
self.sum += other.sum;
if other.min < self.min {
self.min = other.min;
}
if other.max > self.max {
self.max = other.max;
}
}
}
pub type DDSketch128 = DDSketchN<128>;
pub type DDSketch256 = DDSketchN<256>;
pub type DDSketch512 = DDSketchN<512>;
pub type DDSketch1024 = DDSketchN<1024>;
pub type DDSketch2048 = DDSketchN<2048>;
pub type DDSketch = DDSketch2048;
#[derive(Clone, Debug)]
pub struct CountMinSketchN<const W: usize, const D: usize> {
counters: [[u64; W]; D],
total: u64,
}
impl<const W: usize, const D: usize> CountMinSketchN<W, D> {
pub const WIDTH: usize = W;
pub const DEPTH: usize = D;
#[inline]
pub const fn new() -> Self {
Self {
counters: [[0u64; W]; D],
total: 0,
}
}
#[inline]
const fn hash_for_row(hash: u64, row: usize) -> usize {
let h = hash.wrapping_add((row as u64).wrapping_mul(0x9e3779b97f4a7c15));
let mixed = h ^ (h >> 33);
let mixed = mixed.wrapping_mul(0xff51afd7ed558ccd);
let mixed = mixed ^ (mixed >> 33);
(mixed as usize) % W
}
#[inline]
pub fn insert_hash(&mut self, hash: u64, count: u64) {
self.total = self.total.saturating_add(count);
for row in 0..D {
let col = Self::hash_for_row(hash, row);
self.counters[row][col] = self.counters[row][col].saturating_add(count);
}
}
#[inline]
pub fn insert<T: Hash>(&mut self, item: &T) {
let mut hasher = FnvHasher::new();
item.hash(&mut hasher);
self.insert_hash(hasher.finish(), 1);
}
#[inline]
pub fn insert_bytes(&mut self, bytes: &[u8]) {
self.insert_hash(FnvHasher::hash_bytes(bytes), 1);
}
#[inline]
pub fn estimate_hash(&self, hash: u64) -> u64 {
let mut min_count = u64::MAX;
for row in 0..D {
let col = Self::hash_for_row(hash, row);
min_count = min_count.min(self.counters[row][col]);
}
min_count
}
#[inline]
pub fn estimate<T: Hash>(&self, item: &T) -> u64 {
let mut hasher = FnvHasher::new();
item.hash(&mut hasher);
self.estimate_hash(hasher.finish())
}
#[inline]
pub fn estimate_bytes(&self, bytes: &[u8]) -> u64 {
self.estimate_hash(FnvHasher::hash_bytes(bytes))
}
#[inline]
pub const fn total(&self) -> u64 {
self.total
}
#[inline]
pub fn clear(&mut self) {
self.counters = [[0u64; W]; D];
self.total = 0;
}
#[inline]
pub fn error_bound(&self) -> f64 {
core::f64::consts::E / (W as f64)
}
#[inline]
pub fn confidence(&self) -> f64 {
1.0 - crate::det_math::exp64(-(D as f64))
}
}
impl<const W: usize, const D: usize> Default for CountMinSketchN<W, D> {
fn default() -> Self {
Self::new()
}
}
impl<const W: usize, const D: usize> Mergeable for CountMinSketchN<W, D> {
fn merge(&mut self, other: &Self) {
self.total = self.total.saturating_add(other.total);
for row in 0..D {
for col in 0..W {
self.counters[row][col] =
self.counters[row][col].saturating_add(other.counters[row][col]);
}
}
}
}
pub type CountMinSketch1024x5 = CountMinSketchN<1024, 5>;
pub type CountMinSketch2048x7 = CountMinSketchN<2048, 7>;
pub type CountMinSketch4096x5 = CountMinSketchN<4096, 5>;
pub type CountMinSketch = CountMinSketch1024x5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HeavyHitterEntry {
pub hash: u64,
pub count: u64,
}
#[derive(Clone, Debug)]
pub struct HeavyHittersN<const K: usize, const W: usize, const D: usize> {
cms: CountMinSketchN<W, D>,
top_k: [HeavyHitterEntry; K],
count: usize,
}
impl<const K: usize, const W: usize, const D: usize> HeavyHittersN<K, W, D> {
pub const K: usize = K;
#[inline]
pub const fn new() -> Self {
Self {
cms: CountMinSketchN::new(),
top_k: [HeavyHitterEntry { hash: 0, count: 0 }; K],
count: 0,
}
}
pub fn insert_hash(&mut self, hash: u64) {
self.cms.insert_hash(hash, 1);
let estimated = self.cms.estimate_hash(hash);
let mut found_idx = None;
for i in 0..self.count {
if self.top_k[i].hash == hash {
found_idx = Some(i);
break;
}
}
if let Some(idx) = found_idx {
self.top_k[idx].count = estimated;
self.sort_top_k();
} else if self.count < K {
self.top_k[self.count] = HeavyHitterEntry {
hash,
count: estimated,
};
self.count += 1;
self.sort_top_k();
} else if estimated > self.top_k[0].count {
self.top_k[0] = HeavyHitterEntry {
hash,
count: estimated,
};
self.sort_top_k();
}
}
fn sort_top_k(&mut self) {
for i in 1..self.count {
let entry = self.top_k[i];
let mut j = i;
while j > 0 && self.top_k[j - 1].count > entry.count {
self.top_k[j] = self.top_k[j - 1];
j -= 1;
}
self.top_k[j] = entry;
}
}
pub fn top(&self) -> impl Iterator<Item = &HeavyHitterEntry> {
self.top_k[..self.count].iter().rev()
}
#[inline]
pub const fn cms(&self) -> &CountMinSketchN<W, D> {
&self.cms
}
pub fn clear(&mut self) {
self.cms.clear();
self.top_k = [HeavyHitterEntry { hash: 0, count: 0 }; K];
self.count = 0;
}
}
impl<const K: usize, const W: usize, const D: usize> Default for HeavyHittersN<K, W, D> {
fn default() -> Self {
Self::new()
}
}
pub type HeavyHitters10 = HeavyHittersN<10, 1024, 5>;
pub type HeavyHitters20 = HeavyHittersN<20, 2048, 7>;
pub type HeavyHitters5 = HeavyHittersN<5, 1024, 5>;
pub type HeavyHitters = HeavyHitters10;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fnv_hash() {
let h1 = FnvHasher::hash_u64(12345);
let h2 = FnvHasher::hash_u64(12345);
let h3 = FnvHasher::hash_u64(12346);
assert_eq!(h1, h2);
assert_ne!(h1, h3);
}
#[test]
fn test_hyperloglog_basic() {
let mut hll = HyperLogLog16::new();
for i in 0..1000u64 {
hll.insert_hash(FnvHasher::hash_u64(i));
}
let estimate = hll.cardinality();
assert!(
estimate > 800.0 && estimate < 1200.0,
"estimate = {estimate}"
);
}
#[test]
fn test_hyperloglog_merge() {
let mut hll1 = HyperLogLog16::new();
let mut hll2 = HyperLogLog16::new();
for i in 0..500u64 {
hll1.insert_hash(FnvHasher::hash_u64(i));
}
for i in 500..1000u64 {
hll2.insert_hash(FnvHasher::hash_u64(i));
}
hll1.merge(&hll2);
let estimate = hll1.cardinality();
assert!(
estimate > 800.0 && estimate < 1200.0,
"estimate = {estimate}"
);
}
#[test]
fn test_hyperloglog_16_large() {
let mut hll = HyperLogLog16::new();
for i in 0..100000u64 {
hll.insert_hash(FnvHasher::hash_u64(i));
}
let estimate = hll.cardinality();
assert!(
estimate > 75000.0 && estimate < 125000.0,
"estimate = {estimate}"
);
}
#[test]
fn test_ddsketch_basic() {
let mut sketch = DDSketch2048::new(0.01);
let values = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
for v in values {
sketch.insert(v);
}
assert_eq!(sketch.count(), 10);
assert!((sketch.mean() - 55.0).abs() < 0.001);
let p50 = sketch.quantile(0.5);
assert!(p50 > 40.0 && p50 < 70.0, "p50 = {p50}");
let p99 = sketch.quantile(0.99);
assert!(p99 > 80.0 && p99 <= 100.0, "p99 = {p99}");
}
#[test]
fn test_countmin_basic() {
let mut cms = CountMinSketch1024x5::new();
for _ in 0..100 {
cms.insert_hash(1, 1);
}
for _ in 0..50 {
cms.insert_hash(2, 1);
}
for _ in 0..10 {
cms.insert_hash(3, 1);
}
assert!(cms.estimate_hash(1) >= 100);
assert!(cms.estimate_hash(2) >= 50);
assert!(cms.estimate_hash(3) >= 10);
}
#[test]
fn test_countmin_merge() {
let mut cms1 = CountMinSketch1024x5::new();
let mut cms2 = CountMinSketch1024x5::new();
for _ in 0..50 {
cms1.insert_hash(1, 1);
}
for _ in 0..50 {
cms2.insert_hash(1, 1);
}
cms1.merge(&cms2);
assert!(cms1.estimate_hash(1) >= 100);
}
#[test]
fn test_heavy_hitters() {
let mut hh = HeavyHitters5::new();
for _ in 0..100 {
hh.insert_hash(1);
}
for _ in 0..50 {
hh.insert_hash(2);
}
for _ in 0..30 {
hh.insert_hash(3);
}
for _ in 0..10 {
hh.insert_hash(4);
}
for _ in 0..5 {
hh.insert_hash(5);
}
let top: Vec<_> = hh.top().collect();
assert_eq!(top.len(), 5);
assert_eq!(top[0].hash, 1);
assert_eq!(top[1].hash, 2);
assert_eq!(top[2].hash, 3);
}
#[test]
fn countmin_error_bound_is_e_over_width() {
let e = core::f64::consts::E;
let cases: [(f64, usize); 3] = [
(CountMinSketch1024x5::new().error_bound(), 1024),
(CountMinSketch2048x7::new().error_bound(), 2048),
(CountMinSketch4096x5::new().error_bound(), 4096),
];
for (bound, width) in cases {
let expected = e / width as f64;
assert!(
(bound - expected).abs() < 1e-15,
"width={width}: bound={bound} expected={expected}"
);
}
let mut cms = CountMinSketch1024x5::new();
let empty_bound = cms.error_bound();
for i in 0..500u64 {
cms.insert_hash(i, 3);
}
assert!((cms.error_bound() - empty_bound).abs() < 1e-15);
assert!(
CountMinSketch4096x5::new().error_bound() < CountMinSketch1024x5::new().error_bound()
);
}
#[test]
fn countmin_insert_bytes_and_estimate_bytes_roundtrip() {
let mut cms = CountMinSketch2048x7::new();
assert_eq!(cms.estimate_bytes(b"never-inserted"), 0);
for _ in 0..7 {
cms.insert_bytes(b"alpha");
}
for _ in 0..3 {
cms.insert_bytes(b"beta");
}
assert_eq!(cms.total(), 10);
assert_eq!(cms.estimate_bytes(b"alpha"), 7);
assert_eq!(cms.estimate_bytes(b"beta"), 3);
assert_eq!(cms.estimate_bytes(b"gamma"), 0);
let h = FnvHasher::hash_bytes(b"alpha");
assert_eq!(cms.estimate_hash(h), cms.estimate_bytes(b"alpha"));
let mut via_hash = CountMinSketch2048x7::new();
for _ in 0..7 {
via_hash.insert_hash(h, 1);
}
assert_eq!(via_hash.estimate_bytes(b"alpha"), 7);
assert_eq!(via_hash.estimate_bytes(b"beta"), 0);
cms.merge(&via_hash);
assert_eq!(cms.estimate_bytes(b"alpha"), 14);
assert_eq!(cms.estimate_bytes(b"beta"), 3);
assert_eq!(cms.total(), 17);
}
#[test]
fn hyperloglog_insert_bytes_is_idempotent_and_matches_hash_path() {
let mut hll = HyperLogLog10::new();
assert!(hll.cardinality().abs() < 1e-12, "empty HLL → 0");
for _ in 0..1000 {
hll.insert_bytes(b"duplicate");
}
let one = hll.cardinality();
assert!(one > 0.5 && one < 1.5, "cardinality={one}");
let set = hll.registers().iter().filter(|&&r| r != 0).count();
assert_eq!(set, 1, "exactly one register touched");
let mut via_bytes = HyperLogLog10::new();
let mut via_hash = HyperLogLog10::new();
for i in 0..200u32 {
let key = i.to_le_bytes();
via_bytes.insert_bytes(&key);
via_hash.insert_hash(FnvHasher::hash_bytes(&key));
}
assert_eq!(via_bytes.registers(), via_hash.registers());
let est = via_bytes.cardinality();
assert!(est > 150.0 && est < 250.0, "estimate={est}");
}
fn edge_bound(alpha: f64) -> f64 {
2.0 * alpha / (1.0 + alpha)
}
#[test]
fn ddsketch_window_follows_data_past_its_initial_range() {
let alpha = 0.1;
let mut s = DDSketch128::new(alpha);
let values: Vec<f64> = (3..=12)
.map(|e| crate::det_math::powf64(10.0, f64::from(e)))
.collect();
for &v in &values {
s.insert(v);
}
assert_eq!(s.count(), 10);
for (i, &v) in values.iter().enumerate() {
let q = (i as f64 + 0.5) / 10.0;
let est = s.quantile(q);
assert!(
est <= v && (v - est) / v <= edge_bound(alpha),
"q={q} est={est} v={v}"
);
}
assert_eq!((s.min(), s.max()), (values[0], values[9]));
}
#[test]
fn ddsketch_collapses_below_a_full_window_into_its_lowest_bucket() {
let alpha = 0.1;
let gamma = (1.0 + alpha) / (1.0 - alpha);
let mut s = DDSketch128::new(alpha);
s.insert(1.0);
s.insert(1e15);
s.insert(2.0);
let ln_gamma = crate::det_math::ln64(gamma);
let k_top = (crate::det_math::ln64(1e15) / ln_gamma).ceil();
let lowest_edge = crate::det_math::powf64(gamma, k_top - 128.0);
assert_eq!(s.quantile(0.1), lowest_edge);
assert_eq!(s.quantile(0.5), lowest_edge);
let top = s.quantile(1.0);
assert!(
top <= 1e15 && (1e15 - top) / 1e15 <= edge_bound(alpha),
"{top}"
);
assert_eq!(s.count(), 3);
assert_eq!(s.sum(), 1e15 + 3.0);
}
#[test]
fn ddsketch_signed_values_zero_and_rejected_inputs() {
let alpha = 0.01;
let mut s = DDSketch::new(alpha);
assert_eq!(s.quantile(0.5), 0.0);
assert_eq!(s.mean(), 0.0);
s.insert(f64::NAN);
s.insert(f64::INFINITY);
assert_eq!(s.count(), 0);
for v in [-100.0, 0.0, 100.0] {
s.insert(v);
}
let lo = s.quantile(0.1);
assert!(
lo >= -100.0 && (lo + 100.0) / 100.0 <= edge_bound(alpha),
"{lo}"
);
assert_eq!(s.quantile(0.5), 0.0);
let hi = s.quantile(1.0);
assert!(
hi <= 100.0 && (100.0 - hi) / 100.0 <= edge_bound(alpha),
"{hi}"
);
assert_eq!(s.mean(), 0.0);
assert_eq!(s.alpha(), alpha);
s.clear();
assert_eq!((s.count(), s.quantile(0.5)), (0, 0.0));
}
#[test]
fn ddsketch_merge_of_windows_that_moved_apart_adds_by_key() {
let alpha = 0.1;
let (mut a, mut b) = (DDSketch128::new(alpha), DDSketch128::new(alpha));
a.insert(10.0);
b.insert(1e12);
b.insert(-1e12);
a.merge(&b);
assert_eq!(a.count(), 3);
assert_eq!((a.min(), a.max()), (-1e12, 1e12));
let mid = a.quantile(0.5);
assert!(
mid <= 10.0 && (10.0 - mid) / 10.0 <= edge_bound(alpha),
"{mid}"
);
let top = a.quantile(1.0);
assert!(
top <= 1e12 && (1e12 - top) / 1e12 <= edge_bound(alpha),
"{top}"
);
}
#[test]
fn countmin_total_and_counters_saturate_at_u64_max() {
let mut c = CountMinSketch::new();
c.insert_hash(7, u64::MAX - 1);
c.insert_hash(7, 5);
assert_eq!(c.total(), u64::MAX);
assert_eq!(c.estimate_hash(7), u64::MAX);
let mut d = CountMinSketch::default();
d.insert(&"x");
assert_eq!(d.estimate(&"x"), 1);
d.merge(&c);
assert_eq!(d.total(), u64::MAX);
assert_eq!(d.estimate_hash(7), u64::MAX);
let expected = 1.0 - crate::det_math::exp64(-5.0);
assert_eq!(d.confidence(), expected);
d.clear();
assert_eq!((d.total(), d.estimate(&"x")), (0, 0));
}
#[test]
fn heavy_hitters_replace_the_smallest_entry_once_full() {
let mut h = HeavyHittersN::<2, 1024, 5>::default();
for _ in 0..3 {
h.insert_hash(1);
}
h.insert_hash(2);
h.insert_hash(3);
h.insert_hash(3);
let top: Vec<(u64, u64)> = h.top().map(|e| (e.hash, e.count)).collect();
assert_eq!(top, vec![(1, 3), (3, 2)]);
assert_eq!(h.cms().total(), 6);
h.clear();
assert_eq!(h.top().count(), 0);
}
#[test]
fn hasher_trait_and_hyperloglog_generic_insert_agree() {
let mut a = FnvHasher::default();
a.write(b"abc");
assert_eq!(a.finish(), FnvHasher::hash_bytes(b"abc"));
let mut h = HyperLogLog10::default();
h.insert(&42_u64);
h.insert(&42_u64);
assert!((h.cardinality() - 1.0).abs() < 0.01, "{}", h.cardinality());
h.clear();
assert!(h.registers().iter().all(|&r| r == 0));
}
}