pub mod config;
use std::{collections::HashMap, ops::Range};
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use self::config::EventCounterConfig;
use crate::{IntervalConfig, IntervalCounter, Result, TimeUnit};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct SingleEventCounter {
starting_instant: DateTime<Utc>,
intervals: HashMap<TimeUnit, IntervalCounter>,
dirty: bool,
#[cfg_attr(feature = "serde", serde(skip))]
pending: u32,
}
impl SingleEventCounter {
pub(crate) fn new(config: &EventCounterConfig, starting_instant: DateTime<Utc>) -> Self {
let mut intervals = HashMap::new();
for interval_config in config.as_vec() {
let counter = IntervalCounter::new(interval_config.bucket_count(), starting_instant);
intervals.insert(interval_config.time_unit(), counter);
}
Self {
starting_instant,
intervals,
dirty: false,
pending: 0,
}
}
pub fn record(&mut self, count: u32) {
for counter in self.intervals.values_mut() {
counter.increment_bucket(0, count);
}
}
pub(crate) fn record_at(&mut self, count: u32, time: DateTime<Utc>) -> Result<()> {
for (time_unit, counter) in self.intervals.iter_mut() {
counter.increment_at(count, time, time_unit)?;
}
Ok(())
}
pub(crate) fn sum_range(&self, time_unit: TimeUnit, range: Range<usize>) -> Option<u32> {
self.intervals
.get(&time_unit)
.map(|counter| counter.sum_range(range.start, range.end))
}
pub(crate) fn last_seen_in(&self, time_unit: TimeUnit) -> Option<usize> {
let counter = self.intervals.get(&time_unit)?;
(0..counter.bucket_count()).find(|&i| counter.bucket_value(i).unwrap_or(0) > 0)
}
pub(crate) fn first_seen_in(&self, time_unit: TimeUnit) -> Option<usize> {
let counter = self.intervals.get(&time_unit)?;
(0..counter.bucket_count())
.rev()
.find(|&i| counter.bucket_value(i).unwrap_or_default() > 0)
}
pub(crate) fn starting_instant(&self) -> DateTime<Utc> {
self.starting_instant
}
pub(crate) fn interval_start(&self, time_unit: TimeUnit) -> Option<DateTime<Utc>> {
let counter = self.intervals.get(&time_unit)?;
Some(counter.interval_start())
}
pub(crate) fn is_dirty(&self) -> bool {
self.dirty
}
pub(crate) fn mark_dirty(&mut self) {
self.dirty = true;
}
pub(crate) fn reset_dirty(&mut self) {
self.dirty = false;
}
pub fn advance_if_needed(&mut self, now: DateTime<Utc>) {
for (unit, counter) in self.intervals.iter_mut() {
counter.advance_if_needed(unit, now);
}
self.starting_instant = now;
}
pub(crate) fn memory_usage(&self) -> usize {
let mut total = 0usize;
for interval_counter in self.intervals.values() {
total =
total.saturating_add(interval_counter.bucket_count() * std::mem::size_of::<u64>());
}
total
}
pub(crate) fn is_empty(&self, time_unit: TimeUnit) -> bool {
self.sum_range(time_unit, 0..usize::MAX).unwrap_or_default() == 0
}
pub(crate) fn pending_count(&self) -> u32 {
self.pending
}
pub(crate) fn increment_pending(&mut self) {
self.pending = self.pending.saturating_add(1);
}
pub(crate) fn decrement_pending(&mut self) {
self.pending = self.pending.saturating_sub(1);
}
pub(crate) fn intervals(&self) -> &HashMap<TimeUnit, IntervalCounter> {
&self.intervals
}
pub(crate) fn interval(&self, time_unit: TimeUnit) -> Option<&IntervalCounter> {
self.intervals.get(&time_unit)
}
#[cfg(test)]
pub(crate) fn interval_mut(&mut self, time_unit: TimeUnit) -> Option<&mut IntervalCounter> {
self.intervals.get_mut(&time_unit)
}
pub(crate) fn total_with_pending(&self, time_unit: TimeUnit, range: Range<usize>) -> u32 {
let actual_count = self.sum_range(time_unit, range).unwrap_or(0);
actual_count.saturating_add(self.pending_count())
}
pub(crate) fn merge(&mut self, other: SingleEventCounter) -> Result<()> {
let self_config = self.as_counter_config();
let other_converted = crate::config_converter::convert_if_needed(other, &self_config);
let target_instant = self.starting_instant.max(other_converted.starting_instant);
if self.starting_instant < target_instant {
self.advance_if_needed(target_instant);
}
let mut other_aligned = other_converted;
if other_aligned.starting_instant < target_instant {
other_aligned.advance_if_needed(target_instant);
}
for (time_unit, self_counter) in &mut self.intervals {
if let Some(other_counter) = other_aligned.intervals.get(time_unit) {
self_counter.merge(other_counter);
}
}
Ok(())
}
pub(crate) fn as_counter_config(&self) -> EventCounterConfig {
let mut configs: Vec<IntervalConfig> = Default::default();
for (time_unit, counter) in self.intervals.iter() {
let bucket_count = counter.bucket_count();
let res = IntervalConfig::new(bucket_count, *time_unit);
if let Ok(config) = res {
configs.push(config);
}
}
EventCounterConfig::new(configs)
}
#[cfg(test)]
pub(crate) fn bucket_count(&self, time_unit: TimeUnit) -> usize {
self.intervals
.get(&time_unit)
.map(|counter| counter.bucket_count())
.unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn bucket_value(&self, time_unit: TimeUnit, bucket_idx: usize) -> u32 {
self.intervals
.get(&time_unit)
.and_then(|counter| counter.bucket_value(bucket_idx))
.unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn sum_for_unit(&self, time_unit: TimeUnit) -> u32 {
self.sum_range(time_unit, 0..usize::MAX).unwrap_or_default()
}
#[cfg(test)]
pub(crate) fn total_events(&mut self) -> u32 {
let Some(unit) = self.intervals.keys().max() else {
return 0;
};
self.sum_range(*unit, 0..usize::MAX).unwrap_or(0)
}
#[cfg(test)]
pub(crate) fn has_time_unit(&self, time_unit: TimeUnit) -> bool {
self.intervals.contains_key(&time_unit)
}
}
#[cfg(test)]
mod tests {
use chrono::{Duration, TimeZone};
use crate::{sync_time::synchronized_start, Result};
use super::*;
#[test]
fn test_new_creates_counter_with_multiple_intervals() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
IntervalConfig::new_unchecked(7, TimeUnit::Days),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.starting_instant(), start);
assert_eq!(counter.intervals.len(), 3);
assert!(counter.intervals.contains_key(&TimeUnit::Minutes));
assert!(counter.intervals.contains_key(&TimeUnit::Hours));
assert!(counter.intervals.contains_key(&TimeUnit::Days));
}
#[test]
fn test_record_updates_all_intervals_simultaneously() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
IntervalConfig::new_unchecked(7, TimeUnit::Days),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
counter.record(5);
assert_eq!(counter.bucket_value(TimeUnit::Minutes, 0), 5);
assert_eq!(counter.bucket_value(TimeUnit::Hours, 0), 5);
assert_eq!(counter.bucket_value(TimeUnit::Days, 0), 5);
}
#[test]
fn test_record_at_updates_all_intervals_simultaneously() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
let event_time = start - chrono::Duration::minutes(30);
counter.record_at(10, event_time).unwrap();
assert_eq!(counter.bucket_value(TimeUnit::Minutes, 30), 10);
assert_eq!(counter.bucket_value(TimeUnit::Hours, 1), 10);
}
#[test]
fn test_record_at_propagates_errors() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
let future_time = start + chrono::Duration::hours(5);
let result = counter.record_at(10, future_time);
assert!(result.is_err());
}
#[test]
fn test_query_with_minutes_returns_some() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
counter.record(7);
assert_eq!(counter.bucket_value(TimeUnit::Minutes, 0), 7);
}
#[test]
fn test_query_with_untracked_time_unit_returns_none() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.sum_range(TimeUnit::Hours, 0..1), None);
assert_eq!(counter.sum_range(TimeUnit::Days, 0..1), None);
}
#[test]
fn test_last_seen_finds_first_nonzero_bucket() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
let event_time = start - chrono::Duration::minutes(5);
counter.record_at(1, event_time).unwrap();
let result = counter.last_seen_in(TimeUnit::Minutes);
assert_eq!(result, Some(5));
}
#[test]
fn test_last_seen_returns_none_if_all_buckets_zero() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.last_seen_in(TimeUnit::Minutes), None);
}
#[test]
fn test_last_seen_returns_none_for_untracked_time_unit() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.last_seen_in(TimeUnit::Hours), None);
}
#[test]
fn test_first_seen_finds_oldest_nonzero_bucket() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
let event_time_1 = start - chrono::Duration::minutes(10);
counter.record_at(1, event_time_1).unwrap();
let event_time_2 = start - chrono::Duration::minutes(5);
counter.record_at(1, event_time_2).unwrap();
let result = counter.first_seen_in(TimeUnit::Minutes);
assert_eq!(result, Some(10));
let last_result = counter.last_seen_in(TimeUnit::Minutes);
assert_eq!(last_result, Some(5));
}
#[test]
fn test_first_seen_returns_none_if_all_buckets_zero() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.first_seen_in(TimeUnit::Minutes), None);
}
#[test]
fn test_first_seen_returns_none_for_untracked_time_unit() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
assert_eq!(counter.first_seen_in(TimeUnit::Hours), None);
}
#[test]
fn test_first_seen_with_single_event() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
let event_time = start - chrono::Duration::minutes(15);
counter.record_at(1, event_time).unwrap();
let first_result = counter.first_seen_in(TimeUnit::Minutes);
let last_result = counter.last_seen_in(TimeUnit::Minutes);
assert_eq!(first_result, Some(15));
assert_eq!(last_result, Some(15));
}
#[test]
fn test_first_seen_with_current_bucket() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(60, TimeUnit::Minutes)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
counter.record(1);
let result = counter.first_seen_in(TimeUnit::Minutes);
assert_eq!(result, Some(0));
}
#[test]
fn test_first_last_seen_in_worked_example() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 12, 30, 0).unwrap();
let start = synchronized_start(now);
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
IntervalConfig::new_unchecked(28, TimeUnit::Days),
]);
let mut source = SingleEventCounter::new(&source_config, start);
source.advance_if_needed(now);
source.record_at(5, now - Duration::hours(24))?;
source.record_at(4, now - Duration::minutes(90))?;
source.record_at(3, now - Duration::minutes(58))?;
source.record_at(2, now - Duration::minutes(45))?;
source.record_at(1, now)?;
let total = source.total_events();
assert_eq!(
total,
5 + 4 + 3 + 2 + 1,
"Conversion must have actually correct count"
);
assert_eq!(source.last_seen_in(TimeUnit::Days), Some(0)); assert_eq!(source.last_seen_in(TimeUnit::Hours), Some(0)); assert_eq!(source.last_seen_in(TimeUnit::Minutes), Some(0));
assert_eq!(source.first_seen_in(TimeUnit::Days), Some(1)); assert_eq!(source.first_seen_in(TimeUnit::Hours), Some(23)); assert_eq!(source.first_seen_in(TimeUnit::Minutes), Some(58));
Ok(())
}
#[test]
fn test_different_bucket_counts_per_interval() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
IntervalConfig::new_unchecked(7, TimeUnit::Days),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let counter = SingleEventCounter::new(&config, start);
let minutes_counter = counter.intervals.get(&TimeUnit::Minutes).unwrap();
assert_eq!(
minutes_counter.sum_range(0, 60),
0 );
let hours_counter = counter.intervals.get(&TimeUnit::Hours).unwrap();
assert_eq!(hours_counter.sum_range(0, 24), 0);
let days_counter = counter.intervals.get(&TimeUnit::Days).unwrap();
assert_eq!(days_counter.sum_range(0, 7), 0);
}
#[test]
fn test_integration_record_once_query_at_different_granularities() {
let config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(60, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
IntervalConfig::new_unchecked(7, TimeUnit::Days),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
let mut counter = SingleEventCounter::new(&config, start);
counter.record(1);
assert_eq!(counter.bucket_value(TimeUnit::Minutes, 0), 1);
assert_eq!(counter.bucket_value(TimeUnit::Hours, 0), 1);
assert_eq!(counter.bucket_value(TimeUnit::Days, 0), 1);
assert_eq!(counter.sum_range(TimeUnit::Minutes, 0..60), Some(1));
assert_eq!(counter.sum_range(TimeUnit::Hours, 0..24), Some(1));
assert_eq!(counter.sum_range(TimeUnit::Days, 0..7), Some(1));
}
#[test]
fn test_merge_combines_counts() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config, start);
counter1.record(3);
let mut counter2 = SingleEventCounter::new(&config, start);
counter2.record(5);
counter1.merge(counter2).unwrap();
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 8);
}
#[test]
fn test_merge_is_commutative() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter_a1 = SingleEventCounter::new(&config, start);
counter_a1.record(3);
let mut counter_b1 = SingleEventCounter::new(&config, start);
counter_b1.record(5);
let mut counter_a2 = SingleEventCounter::new(&config, start);
counter_a2.record(3);
let mut counter_b2 = SingleEventCounter::new(&config, start);
counter_b2.record(5);
counter_a1.merge(counter_b1).unwrap();
counter_b2.merge(counter_a2).unwrap();
assert_eq!(counter_a1.bucket_value(TimeUnit::Days, 0), 8);
assert_eq!(counter_b2.bucket_value(TimeUnit::Days, 0), 8);
}
#[test]
fn test_merge_is_associative() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter_a1 = SingleEventCounter::new(&config, start);
counter_a1.record(3);
let mut counter_b1 = SingleEventCounter::new(&config, start);
counter_b1.record(5);
let mut counter_c1 = SingleEventCounter::new(&config, start);
counter_c1.record(7);
let mut counter_a2 = SingleEventCounter::new(&config, start);
counter_a2.record(3);
let mut counter_b2 = SingleEventCounter::new(&config, start);
counter_b2.record(5);
let mut counter_c2 = SingleEventCounter::new(&config, start);
counter_c2.record(7);
counter_a1.merge(counter_b1).unwrap();
counter_a1.merge(counter_c1).unwrap();
counter_b2.merge(counter_c2).unwrap();
counter_a2.merge(counter_b2).unwrap();
assert_eq!(counter_a1.bucket_value(TimeUnit::Days, 0), 15);
assert_eq!(counter_a2.bucket_value(TimeUnit::Days, 0), 15);
}
#[test]
fn test_merge_handles_alignment_when_starting_instant_differs() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start1 = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let start2 = Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config, start1);
counter1.record(3);
let mut counter2 = SingleEventCounter::new(&config, start2);
counter2.record(5);
counter1.merge(counter2).unwrap();
assert_eq!(counter1.starting_instant(), start2);
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 5);
assert_eq!(counter1.bucket_value(TimeUnit::Days, 1), 3);
}
#[test]
fn test_merge_preserves_most_recent_starting_instant() {
let config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start1 = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let start2 = Utc.with_ymd_and_hms(2025, 1, 3, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config, start1);
counter1.record(10);
let counter2 = SingleEventCounter::new(&config, start2);
counter1.merge(counter2).unwrap();
assert_eq!(counter1.starting_instant(), start2);
}
#[test]
fn test_merge_with_different_time_units_converts_config() {
let config1 = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(7, TimeUnit::Days),
IntervalConfig::new_unchecked(4, TimeUnit::Weeks),
]);
let config2 = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(7, TimeUnit::Days),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config1, start);
counter1.record(10);
let mut counter2 = SingleEventCounter::new(&config2, start);
counter2.record(5);
let result = counter1.merge(counter2);
assert!(result.is_ok(), "Merge should succeed by converting config");
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 15);
assert_eq!(counter1.bucket_value(TimeUnit::Weeks, 0), 15); }
#[test]
fn test_merge_with_subset_configs_converts() {
let config1 = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(7, TimeUnit::Days),
IntervalConfig::new_unchecked(4, TimeUnit::Weeks),
IntervalConfig::new_unchecked(12, TimeUnit::Months),
]);
let config2 =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config1, start);
counter1.record(10);
let mut counter2 = SingleEventCounter::new(&config2, start);
counter2.record(5);
let result = counter1.merge(counter2);
assert!(result.is_ok(), "Merge should succeed by converting config");
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 15);
assert_eq!(counter1.bucket_value(TimeUnit::Weeks, 0), 15); assert_eq!(counter1.bucket_value(TimeUnit::Months, 0), 15); }
#[test]
fn test_merge_converts_different_bucket_counts() {
let config1 =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let config2 =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(14, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config1, start);
counter1.record(10);
let mut counter2 = SingleEventCounter::new(&config2, start);
counter2.record(5);
let result = counter1.merge(counter2);
assert!(
result.is_ok(),
"Merge should succeed by converting bucket counts"
);
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 15);
}
#[test]
fn test_merge_with_same_time_units_but_different_bucket_counts() {
let config1 =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let config2 =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(14, TimeUnit::Days)]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config1, start);
counter1.record(10);
let mut counter2 = SingleEventCounter::new(&config2, start);
counter2.record(5);
let result = counter1.merge(counter2);
assert!(
result.is_ok(),
"Merge should succeed by converting bucket counts"
);
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 15);
}
#[test]
fn test_merge_with_multiple_time_units_and_different_bucket_counts() {
let config1 = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(7, TimeUnit::Days),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let config2 = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(14, TimeUnit::Days),
IntervalConfig::new_unchecked(48, TimeUnit::Hours),
]);
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let mut counter1 = SingleEventCounter::new(&config1, start);
counter1.record(10);
let mut counter2 = SingleEventCounter::new(&config2, start);
counter2.record(5);
let result = counter1.merge(counter2);
assert!(
result.is_ok(),
"Merge should succeed by converting bucket counts"
);
assert_eq!(counter1.bucket_value(TimeUnit::Days, 0), 15);
assert_eq!(counter1.bucket_value(TimeUnit::Hours, 0), 15);
}
}