use super::interpolator::Interpolator;
use crate::histogram::scale::LogScale;
pub struct CumulativeCount<'a> {
interpolator: Interpolator<'a>,
bucket_index: usize,
accumulated: u64,
}
impl<'a> CumulativeCount<'a> {
pub fn new(log_scale: &'a LogScale, buckets: &'a [u64]) -> Self {
Self {
interpolator: Interpolator::new(log_scale, buckets),
bucket_index: 0,
accumulated: 0,
}
}
pub fn current_bucket(&self) -> usize {
self.bucket_index
}
pub fn whole_bucket_accumulated(&self) -> u64 {
self.accumulated
}
pub fn count_below(&mut self, position: u64) -> f64 {
let num_buckets = self.interpolator.num_buckets();
while self.bucket_index < num_buckets {
let b = self.interpolator.bucket(self.bucket_index);
debug_assert!(
position >= b.left(),
"CumulativeCount::count_below requires monotonically increasing positions"
);
let past_bucket = if b.is_last() {
position > b.right()
} else {
position >= b.right()
};
if past_bucket {
self.accumulated += b.count();
self.bucket_index += 1;
} else {
let partial = self.interpolator.trapezoidal_cdf(self.bucket_index, position.saturating_sub(b.left()));
return self.accumulated as f64 + partial;
}
}
self.accumulated as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::histogram::Histogram;
fn make_hist(records: &[(u64, u64)]) -> Histogram<()> {
let mut hist = Histogram::<()>::new();
for &(value, count) in records {
hist.record_n(value, count);
}
hist
}
fn make_cursor(records: &[(u64, u64)]) -> CumulativeCount<'static> {
let hist = Box::leak(Box::new(make_hist(records)));
hist.cumulative_count()
}
fn count_below_oneshot(records: &[(u64, u64)], position: u64) -> f64 {
let h = make_hist(records);
h.interpolator().count_below(position)
}
#[test]
fn test_cursor_equivalence_with_count_below() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
for pos in [0, 5, 8, 9, 10, 11, 12, 13, 14, 100] {
let from_cursor = cursor.count_below(pos);
let from_density = count_below_oneshot(r, pos);
assert!(
(from_cursor - from_density).abs() < 1e-10,
"pos={pos}: cursor={from_cursor}, density={from_density}"
);
}
}
#[test]
fn test_cursor_same_bucket_calls() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
let c1 = cursor.count_below(10);
let expected1 = count_below_oneshot(r, 10);
assert!((c1 - expected1).abs() < 1e-10);
let c2 = cursor.count_below(11);
let expected2 = count_below_oneshot(r, 11);
assert!((c2 - expected2).abs() < 1e-10);
assert!(c2 >= c1);
}
#[test]
fn test_cursor_bucket_boundary() {
let r = &[(8, 20), (10, 50)];
let mut cursor = make_cursor(r);
let c = cursor.count_below(10);
assert!((c - 20.0).abs() < 1e-10);
let c = cursor.count_below(12);
assert!((c - 70.0).abs() < 1e-10);
}
#[test]
fn test_cursor_past_all_buckets() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
let c = cursor.count_below(u64::MAX);
assert!((c - 60.0).abs() < 1e-10);
}
#[test]
fn test_cursor_empty_histogram() {
let mut cursor = make_cursor(&[]);
let c = cursor.count_below(100);
assert!(c.abs() < 1e-10);
}
#[test]
fn test_cursor_monotonicity() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
let mut prev = 0.0;
for pos in [0, 5, 8, 9, 10, 11, 12, 13, 14, 100] {
let c = cursor.count_below(pos);
assert!(c >= prev, "pos={pos}: {c} < prev={prev}");
prev = c;
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "monotonically increasing")]
fn test_cursor_panics_on_backward() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
cursor.count_below(11);
cursor.count_below(9); }
#[test]
fn test_cursor_position_zero() {
let mut cursor = make_cursor(&[(0, 50), (5, 30)]);
let c = cursor.count_below(0);
assert!(c.abs() < 1e-10);
}
#[test]
fn test_cursor_same_position_twice() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
let c1 = cursor.count_below(11);
let c2 = cursor.count_below(11);
assert!((c1 - c2).abs() < 1e-10);
}
#[test]
fn test_cursor_gap_between_buckets() {
let mut cursor = make_cursor(&[(5, 10), (16, 40)]);
let c = cursor.count_below(10);
assert!((c - 10.0).abs() < 1e-10);
let c = cursor.count_below(18);
assert!(c > 10.0);
assert!(c < 50.0);
}
#[test]
fn test_cursor_first_bucket() {
let mut cursor = make_cursor(&[(0, 10), (1, 20)]);
let c = cursor.count_below(0);
assert!(c.abs() < 1e-10);
let c = cursor.count_below(1);
assert!((c - 10.0).abs() < 1e-10);
let c = cursor.count_below(2);
assert!((c - 30.0).abs() < 1e-10);
}
#[test]
fn test_cursor_last_bucket() {
let mut cursor = make_cursor(&[(0b111 << 61, 100)]);
let c = cursor.count_below(0b110 << 61);
assert!(c.abs() < 1e-10);
let c = cursor.count_below(u64::MAX - 1);
assert!(c > 0.0);
assert!(c <= 100.0);
}
#[test]
fn test_cursor_u64_max_excludes_terminal_endpoint_mass() {
let mut hist = Histogram::<()>::with_log_scale(16, 1);
hist.record_n(u64::MAX, 100);
let hist = Box::leak(Box::new(hist));
let mut cursor = hist.cumulative_count();
let c = cursor.count_below(u64::MAX);
assert!(c > 0.0);
assert!(c < 100.0, "count_below(u64::MAX) should exclude the endpoint");
}
#[test]
fn test_cursor_u64_max_after_earlier_calls() {
let r = &[(8, 10), (10, 20), (12, 30)];
let mut cursor = make_cursor(r);
let c = cursor.count_below(11);
assert!(c > 0.0);
let c = cursor.count_below(u64::MAX);
assert!((c - 60.0).abs() < 1e-10);
}
#[test]
fn test_cursor_single_call_past_all() {
let r = &[(0, 5), (5, 10), (10, 20), (100, 50)];
let h = Box::leak(Box::new(make_hist(r)));
let total = h.total() as f64;
let mut cursor = h.cumulative_count();
let c = cursor.count_below(u64::MAX);
assert!((c - total).abs() < 1e-6);
}
}