use chrono::{DateTime, Utc};
use crate::{
sync_time::synchronized_start, Error, EventCounterConfig, IntervalConfig, IntervalCounter,
SingleEventCounter,
};
pub fn convert_if_needed(
source: SingleEventCounter,
target_config: &EventCounterConfig,
) -> SingleEventCounter {
let source_config = source.as_counter_config();
if &source_config == target_config {
return source;
}
convert(source, &source_config, target_config)
}
fn convert(
source: SingleEventCounter,
source_config: &EventCounterConfig,
target_config: &EventCounterConfig,
) -> SingleEventCounter {
let now = source.starting_instant();
let starting_instant = synchronized_start(now);
let mut target = target_config.create_counter(starting_instant);
target.advance_if_needed(now);
replay_into(source_config, &source, &mut target);
target
}
pub fn replay(source: &SingleEventCounter, target: &mut SingleEventCounter) {
replay_into(&source.as_counter_config(), source, target);
}
fn replay_into(
source_config: &EventCounterConfig,
source: &SingleEventCounter,
target: &mut SingleEventCounter,
) {
let configs = source_config.configs();
let intervals = source.intervals();
if configs.is_empty() {
return;
}
let mut start_bucket: usize = 0;
for interval_idx in 0..configs.len() {
let config = &configs[interval_idx];
let interval = &intervals[&config.time_unit()];
let mut checksum = if let Some(next_config) = configs.get(interval_idx + 1) {
let next_interval = &intervals[&next_config.time_unit()];
Some(Checksum::new(config, interval, next_config, next_interval))
} else {
None
};
transfer_buckets(config, interval, start_bucket, &mut checksum, target);
if let Some(checksum) = checksum {
start_bucket = checksum.last_bucket + 1;
record_gaps(config, interval, checksum, target);
}
}
}
struct Checksum<'a> {
pub(crate) buckets: IntervalCounter,
pub(crate) last_bucket: usize,
next_config: &'a IntervalConfig,
pub(crate) next_interval: &'a IntervalCounter,
}
impl<'a> Checksum<'a> {
fn new(
this_config: &IntervalConfig,
this_interval: &'a IntervalCounter,
next_config: &'a IntervalConfig,
next_interval: &'a IntervalCounter,
) -> Self {
let now = this_config.first_moment_ever(this_interval.interval_start());
let last_bucket = next_config
.time_unit()
.bucket_idx(next_interval.interval_start(), now)
.unwrap_or_default();
let buckets = IntervalCounter::new(last_bucket + 1, next_interval.interval_start());
Self {
buckets,
last_bucket,
next_config,
next_interval,
}
}
fn next_interval_start(&self) -> DateTime<Utc> {
self.next_interval.interval_start()
}
fn record_at(&mut self, count: u32, time: DateTime<Utc>) {
let _ = self
.buckets
.increment_at(count, time, &self.next_config.time_unit());
}
}
fn transfer_buckets(
config: &IntervalConfig,
interval: &IntervalCounter,
start_bucket: usize,
checksum: &mut Option<Checksum<'_>>,
target: &mut SingleEventCounter,
) {
let interval_start = interval.interval_start();
let starting_instant = target.starting_instant();
let time_unit = config.time_unit();
for bucket_idx in 0..config.bucket_count() {
let count = match interval.bucket_value(bucket_idx) {
Some(c) if c > 0 => c,
_ => continue,
};
let bucket_time = time_unit.bucket_time(starting_instant, interval_start, bucket_idx);
if let Some(checksum) = checksum {
checksum.record_at(count, bucket_time);
}
if bucket_idx >= start_bucket {
target
.record_at(count, bucket_time)
.expect("Recording in the future");
}
}
}
fn record_gaps(
config: &IntervalConfig,
interval: &IntervalCounter,
checksum: Checksum<'_>,
target: &mut SingleEventCounter,
) {
let next_interval = checksum.next_interval;
let next = checksum.next_config;
let now = checksum.next_interval_start();
let interval_start = interval.interval_start();
let bucket_idx = checksum.last_bucket;
let calculated = checksum.buckets.bucket_value(bucket_idx).unwrap_or(0);
let actual = match next_interval.bucket_value(bucket_idx) {
Some(count) => count,
None => return,
};
if actual <= calculated {
return;
}
let difference = actual - calculated;
let prev_coverage = config.first_moment_ever(interval_start);
let bucket_end = next.time_unit().bucket_end(now, bucket_idx);
let gap_middle = prev_coverage.signed_duration_since(bucket_end) / 2;
let timestamp = prev_coverage - gap_middle;
if let Err(Error::FutureEvent) = target.record_at(difference, timestamp) {
let _ = target.record_at(difference, now);
}
}
#[cfg(test)]
mod tests {
use chrono::{Duration, TimeZone, Utc};
use super::*;
use crate::{sync_time::synchronized_start, IntervalConfig, Result, TimeUnit};
impl EventCounterConfig {
fn single(bucket_count: usize, time_unit: TimeUnit) -> Self {
Self::new(vec![IntervalConfig::new_unchecked(bucket_count, time_unit)])
}
fn two(
bucket_count1: usize,
time_unit1: TimeUnit,
bucket_count2: usize,
time_unit2: TimeUnit,
) -> Self {
Self::new(vec![
IntervalConfig::new_unchecked(bucket_count1, time_unit1),
IntervalConfig::new_unchecked(bucket_count2, time_unit2),
])
}
}
impl SingleEventCounter {
pub(crate) fn increment_bucket(
&mut self,
time_unit: TimeUnit,
bucket_idx: usize,
count: u32,
) {
self.interval_mut(time_unit)
.unwrap()
.increment_bucket(bucket_idx, count);
}
}
#[test]
fn test_conversion_preserves_starting_instant() {
let start = Utc.with_ymd_and_hms(2025, 6, 15, 12, 30, 45).unwrap();
let source_config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let source = SingleEventCounter::new(&source_config, start);
let target_config = EventCounterConfig::single(14, TimeUnit::Days);
let converted = convert(source, &source_config, &target_config);
assert_eq!(converted.starting_instant(), start);
}
#[test]
fn test_conversion_handles_gaps() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(30, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let mut source = SingleEventCounter::new(&source_config, synchronized_start(now));
source.advance_if_needed(now);
source.record_at(10, now - Duration::minutes(45))?;
let old_total = source.total_events();
let target_config = EventCounterConfig::single(60, TimeUnit::Minutes);
let mut converted = convert(source, &source_config, &target_config);
assert_eq!(
converted.total_events(),
old_total,
"Gap events must be preserved"
);
assert_eq!(
converted.total_events(),
10,
"Converted counter should have data"
);
Ok(())
}
#[test]
fn test_conversion_handles_gaps_offset() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 50, 0).unwrap();
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(30, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let mut source = SingleEventCounter::new(&source_config, synchronized_start(now));
source.advance_if_needed(now);
source.record_at(10, now - Duration::minutes(45))?;
let old_total = source.total_events();
let target_config = EventCounterConfig::single(60, TimeUnit::Minutes);
let mut converted = convert(source, &source_config, &target_config);
assert_eq!(
converted.total_events(),
old_total,
"Gap events must be preserved"
);
assert_eq!(
converted.total_events(),
10,
"Converted counter should have data"
);
Ok(())
}
#[test]
fn test_conversion_adds_missing_time_units() {
let source_config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(7, TimeUnit::Days)]);
let now = Utc.with_ymd_and_hms(2025, 12, 1, 12, 30, 0).unwrap();
let start = synchronized_start(now);
let mut source = SingleEventCounter::new(&source_config, start);
source.advance_if_needed(now);
source.increment_bucket(TimeUnit::Days, 0, 42);
let target_config = EventCounterConfig::two(7, TimeUnit::Days, 24, TimeUnit::Hours);
let converted = convert(source, &source_config, &target_config);
assert_eq!(converted.intervals().len(), 2);
assert!(converted.has_time_unit(TimeUnit::Days));
assert!(converted.has_time_unit(TimeUnit::Hours));
let days_total = converted.sum_for_unit(TimeUnit::Days);
let hours_total = converted.sum_for_unit(TimeUnit::Hours);
assert!(days_total > 0);
assert_eq!(days_total, hours_total);
}
#[test]
fn test_conversion_removes_extra_time_units() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(7, TimeUnit::Days),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let mut source = SingleEventCounter::new(&source_config, start);
source.increment_bucket(TimeUnit::Days, 0, 10);
source.increment_bucket(TimeUnit::Hours, 0, 10);
let old_total = source.total_events();
let target_config = EventCounterConfig::single(7, TimeUnit::Days);
let mut converted = convert(source, &source_config, &target_config);
assert_eq!(converted.intervals().len(), 1);
assert!(converted.has_time_unit(TimeUnit::Days));
assert!(!converted.has_time_unit(TimeUnit::Hours));
assert_eq!(converted.total_events(), old_total);
}
#[test]
fn test_conversion_empty_counter() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let source_config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(5, TimeUnit::Days)]);
let source = SingleEventCounter::new(&source_config, start);
let target_config = EventCounterConfig::two(10, TimeUnit::Days, 24, TimeUnit::Hours);
let converted = convert(source, &source_config, &target_config);
assert_eq!(converted.intervals().len(), 2);
assert_eq!(converted.bucket_count(TimeUnit::Days), 10);
assert_eq!(converted.bucket_count(TimeUnit::Hours), 24);
}
#[test]
fn test_conversion_handles_future_events_gracefully() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let source_config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(10, TimeUnit::Days)]);
let mut source = SingleEventCounter::new(&source_config, start);
source.increment_bucket(TimeUnit::Days, 0, 100);
let target_config = EventCounterConfig::single(20, TimeUnit::Days);
let converted = convert(source, &source_config, &target_config);
assert_eq!(converted.intervals().len(), 1);
}
#[test]
fn test_conversion_skips_events_too_old_for_target() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let source_config =
EventCounterConfig::new(vec![IntervalConfig::new_unchecked(365, TimeUnit::Days)]);
let mut source = SingleEventCounter::new(&source_config, start);
source.increment_bucket(TimeUnit::Days, 300, 50);
let target_config = EventCounterConfig::single(7, TimeUnit::Days);
let converted = convert(source, &source_config, &target_config);
assert_eq!(converted.intervals().len(), 1);
}
#[test]
fn test_conversion_touching_bucket_intervals() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 12, 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(1, now)?; source.record_at(2, now - Duration::minutes(10))?; source.record_at(3, now - Duration::minutes(32))?; source.record_at(4, now - Duration::minutes(58))?; source.record_at(5, now - Duration::minutes(90))?;
let old_total = source.total_events();
assert_eq!(
old_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::Hours), Some(1)); assert_eq!(source.first_seen_in(TimeUnit::Minutes), Some(58));
let target_config = EventCounterConfig::single(360, TimeUnit::Minutes);
let mut target = convert(source, &source_config, &target_config);
let new_total = target.total_events();
let counter = target
.interval(TimeUnit::Minutes)
.expect("Minutes are definitely here");
assert_eq!(
new_total, old_total,
"Conversion must preserve total event count"
);
assert_eq!(
counter.non_zero_idx(),
vec![
0, 9, 31, 57, 75, ],
);
Ok(())
}
#[test]
fn test_conversion_overlapping_bucket_intervals() -> Result<()> {
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(120, TimeUnit::Minutes), IntervalConfig::new_unchecked(24, TimeUnit::Hours), IntervalConfig::new_unchecked(28, TimeUnit::Days),
]);
let now = Utc.with_ymd_and_hms(2025, 12, 1, 12, 30, 0).unwrap();
let start = synchronized_start(now);
let mut source = SingleEventCounter::new(&source_config, start);
source.advance_if_needed(now);
source.record_at(1, now)?; source.record_at(2, now - Duration::minutes(10))?; source.record_at(3, now - Duration::minutes(32))?; source.record_at(4, now - Duration::minutes(58))?; source.record_at(5, now - Duration::minutes(90))?;
let old_total = source.total_events();
assert_eq!(
old_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::Hours), Some(1)); assert_eq!(source.first_seen_in(TimeUnit::Minutes), Some(90));
let target_config = EventCounterConfig::single(360, TimeUnit::Minutes);
let mut target = convert(source, &source_config, &target_config);
let new_total = target.total_events();
let counter = target
.interval(TimeUnit::Minutes)
.expect("Minutes are definitely here");
assert_eq!(
new_total, old_total,
"Conversion must preserve total event count"
);
assert_eq!(
counter.non_zero_idx(),
vec![
0, 9, 31, 57, 89, ],
);
Ok(())
}
#[test]
fn test_conversion_disjoint_bucket_intervals() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 12, 1, 11, 59, 0).unwrap();
let start = synchronized_start(now);
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(45, 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(1, now)?; source.record_at(2, now - Duration::minutes(10))?; source.record_at(3, now - Duration::minutes(32))?; source.record_at(4, now - Duration::minutes(58))?; source.record_at(5, now - Duration::minutes(90))?;
let old_total = source.total_events();
assert_eq!(
old_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::Hours), Some(1)); assert_eq!(source.first_seen_in(TimeUnit::Minutes), Some(32));
let target_config = EventCounterConfig::single(360, TimeUnit::Minutes);
let mut target = convert(source, &source_config, &target_config);
let new_total = target.total_events();
let counter = target
.interval(TimeUnit::Minutes)
.expect("Minutes are definitely here");
assert_eq!(
new_total, old_total,
"Conversion must preserve total event count"
);
assert_eq!(
counter.non_zero_idx(),
vec![
0, 9, 31, 52, 89, ],
);
Ok(())
}
#[test]
fn test_conversion_preserves_count_with_disjoint_intervals() -> Result<()> {
let now = Utc.with_ymd_and_hms(2025, 12, 1, 12, 0, 0).unwrap();
let start = synchronized_start(now);
let source_config = EventCounterConfig::new(vec![
IntervalConfig::new_unchecked(45, TimeUnit::Minutes),
IntervalConfig::new_unchecked(24, TimeUnit::Hours),
]);
let mut source = SingleEventCounter::new(&source_config, start);
source.advance_if_needed(now);
source.record_at(5, now - Duration::minutes(10))?; source.record_at(7, now - Duration::minutes(50))?; source.record_at(3, now - Duration::minutes(90))?;
let source_total = source.total_events();
assert_eq!(source_total, 5 + 7 + 3);
let target_config = EventCounterConfig::single(180, TimeUnit::Minutes);
let mut target = convert(source, &source_config, &target_config);
assert_eq!(
target.total_events(),
source_total,
"Conversion must preserve total event count"
);
Ok(())
}
}