use std::collections::VecDeque;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const SANITY_THRESHOLD: u32 = 50_000_000;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub(crate) struct CountRing {
buckets: VecDeque<u32>,
}
impl std::fmt::Debug for CountRing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.buckets.fmt(f)
}
}
impl CountRing {
pub(crate) fn new(bucket_count: usize) -> Self {
Self {
buckets: VecDeque::from(vec![0; bucket_count]),
}
}
pub(crate) fn increment(&mut self, bucket_index: usize, count: u32) {
if bucket_index >= self.buckets.len() {
return;
}
self.buckets[bucket_index] = self.buckets[bucket_index].saturating_add(count);
#[cfg(debug_assertions)]
{
let new_value = self.buckets[bucket_index];
if new_value > SANITY_THRESHOLD {
eprintln!(
"tiny-counter warning: bucket[{}] has {} events (threshold: {}). \
This library is designed for on-device tracking at reasonable scale. \
Consider if this is the right tool for your use case.",
bucket_index, new_value, SANITY_THRESHOLD
);
}
}
}
pub(crate) fn rotate(&mut self, rotations: usize) {
let bucket_count = self.buckets.len();
let n = rotations.min(bucket_count);
for _ in 0..n {
self.buckets.push_front(0);
}
self.buckets.truncate(bucket_count);
}
pub(crate) fn get(&self, index: usize) -> Option<u32> {
self.buckets.get(index).copied()
}
pub(crate) fn len(&self) -> usize {
self.buckets.len()
}
#[allow(dead_code)]
pub(crate) fn is_empty(&self) -> bool {
self.buckets.is_empty()
}
#[allow(dead_code)]
pub(crate) fn buckets(&self) -> &VecDeque<u32> {
&self.buckets
}
pub(crate) fn sum_range(&self, start: usize, end: usize) -> u32 {
let actual_end = end.min(self.buckets.len());
let actual_start = start.min(actual_end);
self.buckets
.range(actual_start..actual_end)
.fold(0u32, |acc, &val| acc.saturating_add(val))
}
pub fn into_buckets(self) -> Vec<u32> {
self.buckets.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "serde-json")]
fn test_transparent_serialization_is_compact() {
let ring = CountRing::new(5);
let json = serde_json::to_string(&ring).unwrap();
assert_eq!(json, "[0,0,0,0,0]");
let deserialized: CountRing = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.len(), 5);
}
#[test]
fn test_new_creates_ring_with_zero_buckets() {
let ring = CountRing::new(10);
assert_eq!(ring.len(), 10);
assert!(!ring.is_empty());
for i in 0..10 {
assert_eq!(ring.get(i), Some(0));
}
}
#[test]
fn test_increment_bucket_0() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
assert_eq!(ring.len(), 10); assert_eq!(ring.get(0), Some(5));
}
#[test]
fn test_increment_past_buckets() {
let mut ring = CountRing::new(20);
ring.increment(5, 10);
assert_eq!(ring.len(), 20); assert_eq!(ring.get(0), Some(0)); assert_eq!(ring.get(5), Some(10)); }
#[test]
fn test_increment_multiple_times() {
let mut ring = CountRing::new(10);
ring.increment(2, 5);
ring.increment(2, 3);
assert_eq!(ring.get(2), Some(8));
}
#[test]
fn test_increment_beyond_bucket_count() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
ring.increment(15, 100); assert_eq!(ring.len(), 10); assert_eq!(ring.get(0), Some(5));
}
#[test]
fn test_rotate_single() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
ring.increment(1, 3);
ring.rotate(1);
assert_eq!(ring.get(0), Some(0)); assert_eq!(ring.get(1), Some(5)); assert_eq!(ring.get(2), Some(3)); }
#[test]
fn test_rotate_multiple() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
ring.rotate(5);
assert_eq!(ring.len(), 10); assert_eq!(ring.get(0), Some(0));
assert_eq!(ring.get(5), Some(5)); }
#[test]
fn test_rotate_at_bucket_count() {
let mut ring = CountRing::new(5);
ring.increment(0, 10);
ring.increment(1, 20);
ring.rotate(5);
assert_eq!(ring.len(), 5);
assert_eq!(ring.get(0), Some(0));
assert_eq!(ring.get(4), Some(0));
}
#[test]
fn test_rotate_beyond_bucket_count() {
let mut ring = CountRing::new(5);
ring.increment(0, 10);
ring.rotate(100);
assert_eq!(ring.len(), 5);
assert_eq!(ring.sum_range(0, 5), 0);
}
#[test]
fn test_truncate_maintains_invariant() {
let mut ring = CountRing::new(5);
for i in 0..10 {
ring.increment(i, i as u32 + 1);
}
assert_eq!(ring.len(), 5);
}
#[test]
fn test_get_out_of_bounds() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
assert_eq!(ring.get(10), None);
assert_eq!(ring.get(100), None);
}
#[test]
fn test_sum_range_full() {
let mut ring = CountRing::new(10);
ring.increment(0, 10);
ring.increment(1, 20);
ring.increment(2, 30);
assert_eq!(ring.sum_range(0, 3), 60);
}
#[test]
fn test_sum_range_partial() {
let mut ring = CountRing::new(10);
ring.increment(0, 10);
ring.increment(1, 20);
ring.increment(2, 30);
assert_eq!(ring.sum_range(1, 3), 50);
}
#[test]
fn test_sum_range_out_of_bounds() {
let mut ring = CountRing::new(10);
ring.increment(0, 10);
ring.increment(1, 20);
assert_eq!(ring.sum_range(0, 100), 30);
assert_eq!(ring.sum_range(5, 100), 0);
}
#[test]
fn test_sum_range_empty() {
let ring = CountRing::new(10);
assert_eq!(ring.sum_range(0, 10), 0);
}
#[test]
fn test_buckets_reference() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
ring.increment(1, 10);
let buckets = ring.buckets();
assert_eq!(buckets.len(), 10); assert_eq!(buckets[0], 5);
assert_eq!(buckets[1], 10);
for i in buckets.iter().take(10).skip(2) {
assert_eq!(*i, 0);
}
}
#[test]
fn test_invariant_bucket_count() {
let mut ring = CountRing::new(5);
ring.increment(0, 1);
assert!(ring.len() <= 5);
ring.increment(3, 2);
assert!(ring.len() <= 5);
ring.rotate(2);
assert!(ring.len() <= 5);
ring.increment(10, 100); assert!(ring.len() <= 5);
ring.rotate(100); assert!(ring.len() <= 5);
}
#[test]
fn test_property_sum_range_additive() {
let mut ring = CountRing::new(20);
for i in 0..10 {
ring.increment(i, (i + 1) as u32 * 10);
}
let sum_0_5 = ring.sum_range(0, 5);
let sum_5_10 = ring.sum_range(5, 10);
let sum_0_10 = ring.sum_range(0, 10);
assert_eq!(sum_0_10, sum_0_5 + sum_5_10);
}
#[test]
fn test_complex_workflow() {
let mut ring = CountRing::new(10);
ring.increment(0, 5);
ring.increment(1, 10);
ring.increment(2, 15);
ring.rotate(2);
assert_eq!(ring.get(2), Some(5)); assert_eq!(ring.get(3), Some(10)); assert_eq!(ring.get(4), Some(15));
ring.increment(0, 20);
ring.increment(1, 25);
assert_eq!(ring.sum_range(0, 10), 75);
}
#[test]
fn test_increment_overflow_saturates() {
let mut ring = CountRing::new(10);
ring.increment(0, u32::MAX - 100);
assert_eq!(ring.get(0), Some(u32::MAX - 100));
ring.increment(0, 200);
assert_eq!(ring.get(0), Some(u32::MAX));
}
#[test]
fn test_sum_range_saturates_at_u32_max() {
let mut ring = CountRing::new(10);
ring.increment(0, u32::MAX);
ring.increment(1, u32::MAX);
ring.increment(2, u32::MAX);
let sum = ring.sum_range(0, 3);
assert_eq!(sum, u32::MAX);
}
}