pub mod config;
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{CountRing, Result, TimeUnit};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub(crate) struct IntervalCounter {
data: CountRing,
interval_start: DateTime<Utc>,
}
impl IntervalCounter {
pub(crate) fn new(bucket_count: usize, starting_instant: DateTime<Utc>) -> Self {
Self {
data: CountRing::new(bucket_count),
interval_start: starting_instant,
}
}
pub(crate) fn bucket_count(&self) -> usize {
self.data.len()
}
pub(crate) fn increment_at(
&mut self,
count: u32,
time: DateTime<Utc>,
time_unit: &TimeUnit,
) -> Result<()> {
let bucket_idx = time_unit.bucket_idx(self.interval_start, time)?;
self.data.increment(bucket_idx, count);
Ok(())
}
pub(crate) fn advance_if_needed(&mut self, time_unit: &TimeUnit, now: DateTime<Utc>) {
let rotations = time_unit.num_rotations(self.interval_start, now);
if rotations > 0 {
self.data.rotate(rotations as usize);
self.interval_start = time_unit.rotate_start_interval(self.interval_start, rotations);
}
}
pub(crate) fn sum_range(&self, start: usize, end: usize) -> u32 {
self.data.sum_range(start, end)
}
pub(crate) fn increment_bucket(&mut self, idx: usize, count: u32) {
self.data.increment(idx, count)
}
pub(crate) fn merge(&mut self, other: &IntervalCounter) {
for i in 0..other.bucket_count() {
if let Some(other_value) = other.data.get(i) {
self.data.increment(i, other_value);
}
}
}
#[allow(dead_code)]
pub(crate) fn interval_start(&self) -> DateTime<Utc> {
self.interval_start
}
pub(crate) fn bucket_value(&self, bucket_idx: usize) -> Option<u32> {
self.data.get(bucket_idx)
}
#[allow(dead_code)]
pub(crate) fn into_buckets(self) -> Vec<u32> {
self.data.into_buckets()
}
#[cfg(test)]
pub(crate) fn non_zero_idx(&self) -> Vec<usize> {
(0..self.bucket_count())
.filter(|idx| self.bucket_value(*idx).unwrap_or_default() > 0)
.collect()
}
}
#[cfg(test)]
mod tests {
use chrono::{Duration, TimeZone};
use super::*;
use crate::{Error, TimeUnit};
#[test]
fn test_new_creates_counter_with_zero_buckets() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = IntervalCounter::new(10, start);
assert_eq!(counter.bucket_count(), 10); assert_eq!(counter.interval_start(), start);
for i in 0..10 {
assert_eq!(counter.bucket_value(i), Some(0));
}
}
#[test]
fn test_increment_adds_to_bucket_0() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 5);
assert_eq!(counter.sum_range(0, 1), 5);
counter.increment_bucket(0, 3);
assert_eq!(counter.sum_range(0, 1), 8);
}
#[test]
fn test_advance_if_needed_with_no_time_jump() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 5);
counter.advance_if_needed(&TimeUnit::Days, start);
assert_eq!(counter.interval_start(), start);
assert_eq!(counter.sum_range(0, 1), 5);
}
#[test]
fn test_advance_if_needed_with_small_time_jump() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 5);
let now = start + Duration::hours(1);
counter.advance_if_needed(&TimeUnit::Hours, now);
assert_eq!(counter.interval_start(), now);
assert_eq!(counter.sum_range(0, 1), 0); assert_eq!(counter.sum_range(1, 2), 5); }
#[test]
fn test_advance_if_needed_with_large_time_jump() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 5);
let now = start + Duration::days(100);
counter.advance_if_needed(&TimeUnit::Days, now);
assert_eq!(counter.sum_range(0, 10), 0);
}
#[test]
fn test_increment_at_with_past_event_2_days_ago() {
let time_unit = TimeUnit::Days;
let start = Utc.with_ymd_and_hms(2025, 1, 10, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let event_time = start - Duration::days(2);
counter.increment_at(5, event_time, &time_unit).unwrap();
assert_eq!(counter.sum_range(2, 3), 5);
}
#[test]
fn test_increment_at_with_past_event_1_hour_ago() {
let time_unit = TimeUnit::Minutes;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = IntervalCounter::new(60, start);
let event_time = start - Duration::minutes(30);
counter.increment_at(10, event_time, &time_unit).unwrap();
assert_eq!(counter.sum_range(30, 31), 10);
}
#[test]
fn test_increment_at_with_future_event_returns_error() {
let time_unit = TimeUnit::Days;
let start = Utc.with_ymd_and_hms(2025, 1, 10, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let future_time = start + Duration::days(5);
let result = counter.increment_at(5, future_time, &time_unit);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::FutureEvent));
}
#[test]
fn test_increment_at_same_interval_after_starting_instant() {
let time_unit = TimeUnit::Hours;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let event_time = start + Duration::minutes(30);
counter.increment_at(5, event_time, &time_unit).unwrap();
assert_eq!(counter.sum_range(0, 1), 5);
}
#[test]
fn test_increment_at_same_interval_before_starting_instant() {
let time_unit = TimeUnit::Hours;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 30, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let event_time = Utc.with_ymd_and_hms(2025, 1, 1, 10, 15, 0).unwrap();
counter.increment_at(5, event_time, &time_unit).unwrap();
assert_eq!(counter.sum_range(1, 2), 5);
}
#[test]
fn test_query_range_delegates_correctly() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 10);
counter.increment_bucket(1, 20);
counter.increment_bucket(2, 30);
assert_eq!(counter.sum_range(0, 1), 10);
assert_eq!(counter.sum_range(1, 3), 50);
assert_eq!(counter.sum_range(0, 3), 60);
}
#[test]
fn test_property_starting_instant_monotonic() {
let time_unit = TimeUnit::Days;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let instant1 = counter.interval_start();
let now = start + Duration::days(5);
counter.advance_if_needed(&time_unit, now);
let instant2 = counter.interval_start();
assert!(instant2 >= instant1);
let now2 = now + Duration::days(3);
counter.advance_if_needed(&time_unit, now2);
let instant3 = counter.interval_start();
assert!(instant3 >= instant2);
}
#[test]
fn test_property_advance_idempotent() {
let time_unit = TimeUnit::Days;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
counter.increment_bucket(0, 5);
let now = start + Duration::days(3);
counter.advance_if_needed(&time_unit, now);
let instant_after_first = counter.interval_start();
let sum_after_first = counter.sum_range(0, 10);
counter.advance_if_needed(&time_unit, now);
let instant_after_second = counter.interval_start();
let sum_after_second = counter.sum_range(0, 10);
assert_eq!(instant_after_first, instant_after_second);
assert_eq!(sum_after_first, sum_after_second);
}
#[test]
fn test_property_increment_at_then_query() {
let time_unit = TimeUnit::Hours;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(10, start);
let events = [
(start - Duration::hours(1), 10u32),
(start - Duration::hours(3), 20u32),
(start - Duration::hours(5), 30u32),
];
for (time, count) in events.iter() {
counter.increment_at(*count, *time, &time_unit).unwrap();
}
let total = counter.sum_range(0, 20);
assert_eq!(total, 60); }
#[test]
fn test_complex_workflow_with_time_advancement() {
let time_unit = TimeUnit::Days;
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = IntervalCounter::new(7, start);
counter.increment_bucket(0, 5);
assert_eq!(counter.sum_range(0, 1), 5);
let now = start + Duration::days(1);
counter.advance_if_needed(&time_unit, now);
counter.increment_bucket(0, 10);
assert_eq!(counter.sum_range(0, 1), 10); assert_eq!(counter.sum_range(1, 2), 5); assert_eq!(counter.sum_range(0, 2), 15);
let now = start + Duration::days(4);
counter.advance_if_needed(&time_unit, now);
counter.increment_bucket(0, 20);
assert_eq!(counter.sum_range(0, 1), 20); assert_eq!(counter.sum_range(4, 5), 5); assert_eq!(counter.sum_range(0, 7), 35); }
}