use chrono::{DateTime, Duration, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{Error, Result};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum TimeUnit {
Seconds,
Minutes,
Hours,
Days, Weeks, Months, Years, Ever, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeWindow {
pub count: usize,
pub time_unit: TimeUnit,
}
impl From<TimeUnit> for TimeWindow {
fn from(unit: TimeUnit) -> Self {
TimeWindow {
count: 1,
time_unit: unit,
}
}
}
impl From<(usize, TimeUnit)> for TimeWindow {
fn from(window: (usize, TimeUnit)) -> Self {
TimeWindow {
count: window.0,
time_unit: window.1,
}
}
}
impl From<Duration> for TimeWindow {
fn from(dur: Duration) -> Self {
let dur = dur.abs();
if dur.num_days() > 0 {
TimeWindow {
count: dur.num_days() as usize,
time_unit: TimeUnit::Days,
}
} else if dur.num_hours() > 0 {
TimeWindow {
count: dur.num_hours() as usize,
time_unit: TimeUnit::Hours,
}
} else if dur.num_minutes() > 0 {
TimeWindow {
count: dur.num_minutes() as usize,
time_unit: TimeUnit::Minutes,
}
} else {
TimeWindow {
count: dur.num_seconds() as usize,
time_unit: TimeUnit::Minutes,
}
}
}
}
#[cfg(feature = "calendar")]
mod calendar {
use chrono::{DateTime, Datelike, Local, NaiveDate, TimeZone, Utc};
pub(super) fn local_midnight_to_utc(date: NaiveDate) -> DateTime<Utc> {
Local
.from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
.earliest()
.unwrap()
.with_timezone(&Utc)
}
pub(super) fn local_time_to_utc(date: NaiveDate, hour: u32) -> DateTime<Utc> {
Local
.from_local_datetime(&date.and_hms_opt(hour, 0, 0).unwrap())
.earliest()
.unwrap()
.with_timezone(&Utc)
}
pub(super) fn find_monday(date: NaiveDate) -> NaiveDate {
let days_since_monday = date.weekday().num_days_from_monday();
date - chrono::Days::new(days_since_monday as u64)
}
pub(super) fn add_months(year: i32, month: u32, delta: i32) -> (i32, u32) {
let total_month = month as i32 + delta;
let month0 = total_month - 1;
let year_offset = month0.div_euclid(12);
let final_year = year + year_offset;
let final_month = (month0.rem_euclid(12) + 1) as u32;
(final_year, final_month)
}
}
impl TimeUnit {
pub fn duration(&self) -> Duration {
match self {
Self::Seconds => Duration::seconds(1),
Self::Minutes => Duration::minutes(1),
Self::Hours => Duration::hours(1),
Self::Days => Duration::days(1),
Self::Weeks => Duration::weeks(1),
Self::Months => Duration::days(30),
Self::Years => Duration::days(365),
Self::Ever => {
panic!("TimeUnit::Ever has no fixed duration. Resolve to concrete time unit first.")
}
}
}
pub(crate) fn num_rotations(&self, then: DateTime<Utc>, now: DateTime<Utc>) -> i64 {
#[cfg(feature = "calendar")]
{
use chrono::{Datelike, Local};
match self {
TimeUnit::Days => {
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
return (now_local - then_local).num_days();
}
TimeUnit::Weeks => {
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
let week_diff =
now_local.iso_week().week() as i64 - then_local.iso_week().week() as i64;
let year_diff = (now_local.iso_week().year() as i64
- then_local.iso_week().year() as i64)
* 52;
return week_diff + year_diff;
}
TimeUnit::Months => {
let then_local = then.with_timezone(&Local);
let now_local = now.with_timezone(&Local);
let then_months = then_local.year() as i64 * 12 + then_local.month() as i64;
let now_months = now_local.year() as i64 * 12 + now_local.month() as i64;
return now_months - then_months;
}
TimeUnit::Years => {
let then_local = then.with_timezone(&Local);
let now_local = now.with_timezone(&Local);
return (now_local.year() - then_local.year()) as i64;
}
_ => {} }
}
let duration = now.signed_duration_since(then);
let unit_duration = self.duration();
duration.num_seconds() / unit_duration.num_seconds()
}
pub(crate) fn rotate_start_interval(
&self,
interval_start: DateTime<Utc>,
rotations: i64,
) -> DateTime<Utc> {
#[cfg(feature = "calendar")]
{
use chrono::{Datelike, Local, TimeZone};
match self {
TimeUnit::Days => {
let local = interval_start.with_timezone(&Local);
let target_date = local.date_naive() + chrono::Days::new(rotations as u64);
return calendar::local_midnight_to_utc(target_date);
}
TimeUnit::Weeks => {
let local = interval_start.with_timezone(&Local);
let this_monday = calendar::find_monday(local.date_naive());
let target_monday = this_monday + chrono::Days::new((rotations * 7) as u64);
return calendar::local_midnight_to_utc(target_monday);
}
TimeUnit::Months => {
let local = interval_start.with_timezone(&Local);
let (final_year, final_month) =
calendar::add_months(local.year(), local.month(), rotations as i32);
return Local
.with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
TimeUnit::Years => {
let local = interval_start.with_timezone(&Local);
let target_year = local.year() + rotations as i32;
return Local
.with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
_ => {} }
}
let duration = self.duration();
interval_start + duration * rotations as i32
}
pub(crate) fn bucket_idx(
&self,
interval_start: DateTime<Utc>,
time: DateTime<Utc>,
) -> Result<usize> {
let rotations = self.num_rotations(time, interval_start);
if rotations < 0 {
return Err(Error::FutureEvent);
}
Ok(if rotations == 0 {
if time >= interval_start {
0 } else {
1 }
} else {
rotations as usize
})
}
pub(crate) fn bucket_end(&self, now: DateTime<Utc>, bucket_idx: usize) -> DateTime<Utc> {
if bucket_idx == 0 {
return now;
}
#[cfg(feature = "calendar")]
{
use chrono::{Datelike, Local, TimeZone};
match self {
TimeUnit::Days => {
let now_local = now.with_timezone(&Local);
let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
return calendar::local_midnight_to_utc(target_date);
}
TimeUnit::Weeks => {
let now_local = now.with_timezone(&Local);
let this_monday = calendar::find_monday(now_local.date_naive());
let target_monday = this_monday - chrono::Days::new((bucket_idx * 7) as u64);
return calendar::local_midnight_to_utc(target_monday);
}
TimeUnit::Months => {
let now_local = now.with_timezone(&Local);
let (final_year, final_month) = calendar::add_months(
now_local.year(),
now_local.month(),
-(bucket_idx as i32),
);
return Local
.with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
TimeUnit::Years => {
let now_local = now.with_timezone(&Local);
let target_year = now_local.year() - bucket_idx as i32;
return Local
.with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
_ => {} }
}
now - (self.duration() * bucket_idx as i32)
}
pub(crate) fn bucket_start(&self, now: DateTime<Utc>, bucket_idx: usize) -> DateTime<Utc> {
self.bucket_end(now, bucket_idx + 1)
}
#[allow(dead_code)] pub(crate) fn bucket_duration(&self, now: DateTime<Utc>, bucket_idx: usize) -> Duration {
let bucket_start = self.bucket_start(now, bucket_idx);
let bucket_end = self.bucket_end(now, bucket_idx);
bucket_end - bucket_start
}
pub(crate) fn bucket_midway(
&self,
clock_now: DateTime<Utc>,
interval_start: DateTime<Utc>,
bucket_idx: usize,
) -> DateTime<Utc> {
if bucket_idx == 0 {
let elapsed = clock_now - interval_start;
return interval_start + (elapsed / 2);
}
#[cfg(feature = "calendar")]
{
use chrono::Local;
match self {
TimeUnit::Days => {
let now_local = clock_now.with_timezone(&Local);
let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
return calendar::local_time_to_utc(target_date, 12);
}
TimeUnit::Weeks | TimeUnit::Months | TimeUnit::Years => {
let bucket_start = self.bucket_start(clock_now, bucket_idx);
let duration = self.bucket_duration(clock_now, bucket_idx);
return bucket_start + (duration / 2);
}
_ => {} }
}
let duration = self.duration();
interval_start - (duration * bucket_idx as i32 + duration / 2)
}
pub(crate) fn first_moment_ever(
&self,
now: DateTime<Utc>,
bucket_count: usize,
) -> DateTime<Utc> {
#[cfg(feature = "calendar")]
{
use chrono::{Datelike, Local, TimeZone};
match self {
TimeUnit::Days => {
let now_local = now.with_timezone(&Local);
let target_date =
now_local.date_naive() - chrono::Days::new(bucket_count as u64);
return calendar::local_midnight_to_utc(target_date);
}
TimeUnit::Weeks => {
let now_local = now.with_timezone(&Local);
let this_monday = calendar::find_monday(now_local.date_naive());
let target_monday = this_monday - chrono::Days::new((bucket_count * 7) as u64);
return calendar::local_midnight_to_utc(target_monday);
}
TimeUnit::Months => {
let now_local = now.with_timezone(&Local);
let (final_year, final_month) = calendar::add_months(
now_local.year(),
now_local.month(),
-(bucket_count as i32),
);
return Local
.with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
TimeUnit::Years => {
let now_local = now.with_timezone(&Local);
let target_year = now_local.year() - bucket_count as i32;
return Local
.with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
.earliest()
.unwrap()
.with_timezone(&Utc);
}
_ => {} }
}
now - (self.duration() * bucket_count as i32)
}
pub(crate) fn bucket_time(
&self,
clock_now: DateTime<Utc>,
interval_start: DateTime<Utc>,
bucket_idx: usize,
) -> DateTime<Utc> {
if bucket_idx == 0 {
let elapsed = clock_now - interval_start;
return interval_start + (elapsed / 2);
}
#[cfg(feature = "calendar")]
{
use chrono::Local;
match self {
TimeUnit::Days => {
let now_local = clock_now.with_timezone(&Local);
let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
return calendar::local_time_to_utc(target_date, 12);
}
TimeUnit::Weeks | TimeUnit::Months | TimeUnit::Years => {
let bucket_start = self.bucket_start(clock_now, bucket_idx);
let duration = self.bucket_duration(clock_now, bucket_idx);
return bucket_start + (duration / 2);
}
_ => {} }
}
let duration = self.duration();
interval_start - (duration * bucket_idx as i32) + duration / 2
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[cfg(feature = "calendar")]
mod calendar_helpers {
use super::*;
use chrono::Local;
pub fn calendar_days_between(then: DateTime<Utc>, now: DateTime<Utc>) -> i64 {
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
(now_local - then_local).num_days()
}
}
#[test]
fn test_duration_minutes() {
assert_eq!(TimeUnit::Minutes.duration(), Duration::minutes(1));
}
#[test]
fn test_duration_hours() {
assert_eq!(TimeUnit::Hours.duration(), Duration::hours(1));
}
#[test]
fn test_duration_days() {
assert_eq!(TimeUnit::Days.duration(), Duration::days(1));
}
#[test]
fn test_duration_weeks() {
assert_eq!(TimeUnit::Weeks.duration(), Duration::weeks(1));
}
#[test]
fn test_duration_months() {
assert_eq!(TimeUnit::Months.duration(), Duration::days(30));
}
#[test]
fn test_duration_years() {
assert_eq!(TimeUnit::Years.duration(), Duration::days(365));
}
#[test]
fn test_num_rotations_same_time() {
let time = Utc::now();
assert_eq!(TimeUnit::Days.num_rotations(time, time), 0);
}
#[test]
fn test_num_rotations_past_time() {
let now = Utc.with_ymd_and_hms(2025, 1, 10, 12, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
assert_eq!(TimeUnit::Days.num_rotations(then, now), 5);
}
#[test]
fn test_num_rotations_future_time() {
let now = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
let future = Utc.with_ymd_and_hms(2025, 1, 10, 12, 0, 0).unwrap();
assert_eq!(TimeUnit::Days.num_rotations(future, now), -5);
}
#[test]
fn test_num_rotations_hours() {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
assert_eq!(TimeUnit::Hours.num_rotations(then, now), 5);
}
#[test]
fn test_num_rotations_seconds() {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 45).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
assert_eq!(TimeUnit::Seconds.num_rotations(then, now), 45);
}
#[test]
fn test_num_rotations_minutes() {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 30, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
assert_eq!(TimeUnit::Minutes.num_rotations(then, now), 30);
}
#[test]
fn test_num_rotations_weeks() {
let now = Utc.with_ymd_and_hms(2025, 1, 22, 0, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
assert_eq!(TimeUnit::Weeks.num_rotations(then, now), 3);
}
#[test]
#[cfg(not(feature = "calendar"))]
fn test_num_rotations_months() {
let now = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let days_diff = (now - then).num_days();
assert_eq!(days_diff, 90);
assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_months_calendar_year_boundary() {
let then = Utc.with_ymd_and_hms(2024, 12, 31, 12, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let rotations = TimeUnit::Months.num_rotations(then, now);
assert_eq!(
rotations, 3,
"Should be 3 months from Dec 31, 2024 to March 1, 2025"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_months_calendar_multi_year_span() {
let then = Utc.with_ymd_and_hms(2023, 12, 15, 0, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 12, 1, 0, 0, 0).unwrap();
let rotations = TimeUnit::Months.num_rotations(then, now);
assert_eq!(
rotations, 24,
"Should be 24 months from Dec 2023 to Dec 2025"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_months_calendar() {
let now = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
let now = Utc.with_ymd_and_hms(2025, 4, 15, 12, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_weeks_calendar_year_boundary() {
use chrono::Datelike;
let then = Utc.with_ymd_and_hms(2024, 12, 31, 12, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
use chrono::Local;
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
assert_eq!(
then_local.iso_week().year(),
2025,
"Dec 31, 2024 should be in ISO week year 2025"
);
assert_eq!(
then_local.iso_week().week(),
1,
"Dec 31, 2024 should be in ISO week 1"
);
assert_eq!(
now_local.iso_week().week(),
9,
"March 1, 2025 should be in ISO week 9"
);
let rotations = TimeUnit::Weeks.num_rotations(then, now);
assert_eq!(
rotations, 8,
"Should be 8 ISO weeks from Dec 31, 2024 to March 1, 2025"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_weeks_calendar_multi_year_span() {
use chrono::Datelike;
let then = Utc.with_ymd_and_hms(2024, 12, 30, 0, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 12, 29, 0, 0, 0).unwrap();
use chrono::Local;
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
assert_eq!(
then_local.iso_week().year(),
2025,
"Dec 30, 2024 should be in ISO week year 2025"
);
assert_eq!(
then_local.iso_week().week(),
1,
"Dec 30, 2024 should be in ISO week 1"
);
assert_eq!(
now_local.iso_week().year(),
2026,
"Dec 29, 2025 should be in ISO week year 2026"
);
assert_eq!(
now_local.iso_week().week(),
1,
"Dec 29, 2025 should be in ISO week 1"
);
let rotations = TimeUnit::Weeks.num_rotations(then, now);
assert_eq!(
rotations, 52,
"Should be 52 ISO weeks from Dec 30, 2024 to Dec 29, 2025"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_days_calendar() {
use calendar_helpers::calendar_days_between;
let then = Utc.with_ymd_and_hms(2025, 1, 1, 23, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 1, 5, 1, 0, 0).unwrap();
let expected_days = calendar_days_between(then, now);
let rotations = TimeUnit::Days.num_rotations(then, now);
assert_eq!(
rotations, expected_days,
"Expected {} calendar days (local timezone), got {}",
expected_days, rotations
);
}
#[test]
fn test_num_rotations_years() {
let now = Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
assert_eq!(TimeUnit::Years.num_rotations(then, now), 2);
}
#[test]
fn test_num_rotations_partial_unit() {
let now = Utc.with_ymd_and_hms(2025, 1, 1, 12, 30, 0).unwrap();
let then = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
assert_eq!(TimeUnit::Hours.num_rotations(then, now), 0);
}
#[test]
fn test_num_rotations_transitive() {
let a = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let b = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
let c = Utc.with_ymd_and_hms(2025, 1, 10, 0, 0, 0).unwrap();
let ab = TimeUnit::Days.num_rotations(a, b);
let bc = TimeUnit::Days.num_rotations(b, c);
let ac = TimeUnit::Days.num_rotations(a, c);
assert_eq!(ab + bc, ac);
}
#[test]
fn test_time_unit_is_copy() {
let unit = TimeUnit::Days;
let _unit2 = unit; let _unit3 = unit; }
#[test]
fn test_time_unit_is_eq() {
assert_eq!(TimeUnit::Days, TimeUnit::Days);
assert_ne!(TimeUnit::Days, TimeUnit::Hours);
}
#[test]
fn test_time_unit_is_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(TimeUnit::Days);
assert!(set.contains(&TimeUnit::Days));
}
#[test]
fn test_large_time_jump() {
let now = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
let then = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let rotations = TimeUnit::Days.num_rotations(then, now);
assert!(rotations > 2000); }
#[test]
fn test_time_window_from_time_unit() {
let window: TimeWindow = TimeUnit::Days.into();
assert_eq!(window.count, 1);
assert_eq!(window.time_unit, TimeUnit::Days);
}
#[test]
fn test_time_window_from_tuple() {
let window: TimeWindow = (7, TimeUnit::Days).into();
assert_eq!(window.count, 7);
assert_eq!(window.time_unit, TimeUnit::Days);
}
#[test]
fn test_time_window_from_duration_days() {
let window: TimeWindow = Duration::days(7).into();
assert_eq!(window.count, 7);
assert_eq!(window.time_unit, TimeUnit::Days);
}
#[test]
fn test_time_window_from_duration_hours() {
let window: TimeWindow = Duration::hours(12).into();
assert_eq!(window.count, 12);
assert_eq!(window.time_unit, TimeUnit::Hours);
}
#[test]
fn test_time_window_from_duration_minutes() {
let window: TimeWindow = Duration::minutes(30).into();
assert_eq!(window.count, 30);
assert_eq!(window.time_unit, TimeUnit::Minutes);
}
#[test]
fn test_time_window_from_duration_seconds() {
let window: TimeWindow = Duration::seconds(45).into();
assert_eq!(window.count, 45);
assert_eq!(window.time_unit, TimeUnit::Minutes);
}
#[test]
fn test_time_unit_ord() {
assert!(TimeUnit::Minutes < TimeUnit::Hours);
assert!(TimeUnit::Hours < TimeUnit::Days);
assert!(TimeUnit::Days < TimeUnit::Weeks);
assert!(TimeUnit::Weeks < TimeUnit::Months);
assert!(TimeUnit::Months < TimeUnit::Years);
}
#[test]
fn test_time_unit_ord_transitive() {
assert!(TimeUnit::Minutes < TimeUnit::Days);
assert!(TimeUnit::Hours < TimeUnit::Weeks);
assert!(TimeUnit::Days < TimeUnit::Years);
}
#[test]
fn test_time_unit_ord_reflexive() {
assert!(TimeUnit::Minutes <= TimeUnit::Minutes);
assert!(TimeUnit::Hours >= TimeUnit::Hours);
}
#[test]
fn test_time_unit_ever_is_largest() {
assert!(TimeUnit::Years < TimeUnit::Ever);
assert!(TimeUnit::Minutes < TimeUnit::Ever);
assert!(TimeUnit::Days < TimeUnit::Ever);
}
#[test]
#[should_panic(expected = "TimeUnit::Ever has no fixed duration")]
fn test_time_unit_ever_duration_panics() {
TimeUnit::Ever.duration();
}
#[test]
fn test_time_window_from_negative_duration_days() {
let window: TimeWindow = Duration::days(-7).into();
assert_eq!(window.count, 7);
assert_eq!(window.time_unit, TimeUnit::Days);
}
#[test]
fn test_time_window_from_negative_duration_hours() {
let window: TimeWindow = Duration::hours(-12).into();
assert_eq!(window.count, 12);
assert_eq!(window.time_unit, TimeUnit::Hours);
}
#[test]
fn test_time_window_from_negative_duration_minutes() {
let window: TimeWindow = Duration::minutes(-30).into();
assert_eq!(window.count, 30);
assert_eq!(window.time_unit, TimeUnit::Minutes);
}
#[test]
fn test_time_window_from_negative_duration_seconds() {
let window: TimeWindow = Duration::seconds(-45).into();
assert_eq!(window.count, 45);
assert_eq!(window.time_unit, TimeUnit::Minutes);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_end_days_calendar() {
use chrono::{Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
assert_eq!(TimeUnit::Days.bucket_end(now, 0), now);
let bucket_end = TimeUnit::Days.bucket_end(now, 1);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
assert_eq!(local_bucket.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_end_months_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
assert_eq!(TimeUnit::Months.bucket_end(now, 0), now);
let bucket_end = TimeUnit::Months.bucket_end(now, 1);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.month(), 3);
assert_eq!(local_bucket.day(), 1);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
assert_eq!(local_bucket.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_start_calendar() {
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
let bucket_0_start = TimeUnit::Months.bucket_start(now, 0);
let bucket_1_end = TimeUnit::Months.bucket_end(now, 1);
assert_eq!(bucket_0_start, bucket_1_end);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_start_months_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
let bucket_1_start = TimeUnit::Months.bucket_start(now, 1);
let local_start = bucket_1_start.with_timezone(&Local);
assert_eq!(local_start.month(), 2);
assert_eq!(local_start.day(), 1);
assert_eq!(local_start.hour(), 0);
assert_eq!(local_start.minute(), 0);
assert_eq!(local_start.second(), 0);
let bucket_2_end = TimeUnit::Months.bucket_end(now, 2);
assert_eq!(bucket_1_start, bucket_2_end);
}
#[test]
#[cfg(feature = "calendar")]
fn test_rotate_start_interval_days_calendar() {
use chrono::{Datelike, Local, Timelike};
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let rotated = TimeUnit::Days.rotate_start_interval(start, 3);
let local_rotated = rotated.with_timezone(&Local);
assert_eq!(local_rotated.day(), 4);
assert_eq!(local_rotated.hour(), 0);
assert_eq!(local_rotated.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_rotate_start_interval_months_calendar() {
use chrono::{Datelike, Local, Timelike};
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let rotated = TimeUnit::Months.rotate_start_interval(start, 3);
let local_rotated = rotated.with_timezone(&Local);
assert_eq!(local_rotated.month(), 4);
assert_eq!(local_rotated.day(), 1);
assert_eq!(local_rotated.hour(), 0);
assert_eq!(local_rotated.minute(), 0);
let start = Utc.with_ymd_and_hms(2025, 11, 1, 0, 0, 0).unwrap();
let rotated = TimeUnit::Months.rotate_start_interval(start, 3);
let local_rotated = rotated.with_timezone(&Local);
assert_eq!(local_rotated.year(), 2026);
assert_eq!(local_rotated.month(), 2);
assert_eq!(local_rotated.day(), 1);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_midway_days_calendar() {
use chrono::{Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
let interval_start = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
let midway_0 = TimeUnit::Days.bucket_midway(now, interval_start, 0);
let expected_0 = interval_start + (now - interval_start) / 2;
assert_eq!(midway_0, expected_0);
let midway_1 = TimeUnit::Days.bucket_midway(now, interval_start, 1);
let local_midway = midway_1.with_timezone(&Local);
assert_eq!(local_midway.hour(), 12);
assert_eq!(local_midway.minute(), 0);
assert_eq!(local_midway.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_midway_months_calendar() {
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
let interval_start = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
let midway_1 = TimeUnit::Months.bucket_midway(now, interval_start, 1);
let bucket_start = TimeUnit::Months.bucket_start(now, 1);
let bucket_end = TimeUnit::Months.bucket_end(now, 1);
let expected = bucket_start + (bucket_end - bucket_start) / 2;
assert_eq!(midway_1, expected);
}
#[test]
#[cfg(feature = "calendar")]
fn test_first_moment_ever_days_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 1, 10, 14, 30, 0).unwrap();
let first = TimeUnit::Days.first_moment_ever(now, 5);
let local_first = first.with_timezone(&Local);
let now_local = now.with_timezone(&Local);
assert_eq!(local_first.day(), now_local.day() - 5);
assert_eq!(local_first.hour(), 0);
assert_eq!(local_first.minute(), 0);
assert_eq!(local_first.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_first_moment_ever_months_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
let first = TimeUnit::Months.first_moment_ever(now, 3);
let local_first = first.with_timezone(&Local);
assert_eq!(local_first.month(), 1);
assert_eq!(local_first.day(), 1);
assert_eq!(local_first.hour(), 0);
assert_eq!(local_first.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_time_days_calendar() {
use chrono::{Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
let interval_start = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
let bucket_time_1 = TimeUnit::Days.bucket_time(now, interval_start, 1);
let local_time = bucket_time_1.with_timezone(&Local);
assert_eq!(local_time.hour(), 12);
assert_eq!(local_time.minute(), 0);
assert_eq!(local_time.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_end_weeks_calendar() {
use chrono::{Datelike, Local, Timelike, Weekday};
let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
assert_eq!(TimeUnit::Weeks.bucket_end(now, 0), now);
let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.weekday(), Weekday::Mon);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
assert_eq!(local_bucket.second(), 0);
let now_local = now.with_timezone(&Local);
let this_monday = calendar::find_monday(now_local.date_naive());
let expected_monday = this_monday - chrono::Days::new(7);
assert_eq!(local_bucket.date_naive(), expected_monday);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_midway_weeks_calendar() {
let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
let interval_start = Utc.with_ymd_and_hms(2025, 1, 13, 0, 0, 0).unwrap();
let midway_1 = TimeUnit::Weeks.bucket_midway(now, interval_start, 1);
let bucket_start = TimeUnit::Weeks.bucket_start(now, 1);
let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
let expected = bucket_start + (bucket_end - bucket_start) / 2;
assert_eq!(
midway_1, expected,
"Week midpoint should be bucket_start + duration/2"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_rotate_start_interval_weeks_calendar() {
use chrono::{Datelike, Local, Timelike, Weekday};
let start = Utc.with_ymd_and_hms(2025, 1, 6, 0, 0, 0).unwrap();
let rotated = TimeUnit::Weeks.rotate_start_interval(start, 2);
let local_rotated = rotated.with_timezone(&Local);
assert_eq!(local_rotated.weekday(), Weekday::Mon);
assert_eq!(local_rotated.day(), 20);
assert_eq!(local_rotated.hour(), 0);
assert_eq!(local_rotated.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_first_moment_ever_weeks_calendar() {
use chrono::{Datelike, Local, Timelike, Weekday};
let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
let first = TimeUnit::Weeks.first_moment_ever(now, 3);
let local_first = first.with_timezone(&Local);
assert_eq!(local_first.weekday(), Weekday::Mon);
assert_eq!(local_first.hour(), 0);
assert_eq!(local_first.minute(), 0);
let now_local = now.with_timezone(&Local);
let this_monday = calendar::find_monday(now_local.date_naive());
let expected_monday = this_monday - chrono::Days::new(3 * 7);
assert_eq!(local_first.date_naive(), expected_monday);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_end_years_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
assert_eq!(TimeUnit::Years.bucket_end(now, 0), now);
let bucket_end = TimeUnit::Years.bucket_end(now, 1);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.year(), 2024);
assert_eq!(local_bucket.month(), 1);
assert_eq!(local_bucket.day(), 1);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
assert_eq!(local_bucket.second(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_bucket_midway_years_calendar() {
let now = Utc.with_ymd_and_hms(2025, 6, 15, 14, 30, 0).unwrap();
let interval_start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let midway_1 = TimeUnit::Years.bucket_midway(now, interval_start, 1);
let bucket_start = TimeUnit::Years.bucket_start(now, 1);
let bucket_end = TimeUnit::Years.bucket_end(now, 1);
let expected = bucket_start + (bucket_end - bucket_start) / 2;
assert_eq!(
midway_1, expected,
"Year midpoint should be bucket_start + duration/2"
);
}
#[test]
#[cfg(feature = "calendar")]
fn test_rotate_start_interval_years_calendar() {
use chrono::{Datelike, Local, Timelike};
let start = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
let rotated = TimeUnit::Years.rotate_start_interval(start, 2);
let local_rotated = rotated.with_timezone(&Local);
assert_eq!(local_rotated.year(), 2025);
assert_eq!(local_rotated.month(), 1);
assert_eq!(local_rotated.day(), 1);
assert_eq!(local_rotated.hour(), 0);
assert_eq!(local_rotated.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_first_moment_ever_years_calendar() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 6, 15, 14, 30, 0).unwrap();
let first = TimeUnit::Years.first_moment_ever(now, 3);
let local_first = first.with_timezone(&Local);
assert_eq!(local_first.year(), 2022);
assert_eq!(local_first.month(), 1);
assert_eq!(local_first.day(), 1);
assert_eq!(local_first.hour(), 0);
assert_eq!(local_first.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_week_spanning_year_boundary() {
use chrono::{Datelike, Local, Timelike, Weekday};
let now = Utc.with_ymd_and_hms(2025, 1, 3, 10, 0, 0).unwrap();
let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.year(), 2024);
assert_eq!(local_bucket.month(), 12);
assert_eq!(local_bucket.day(), 23);
assert_eq!(local_bucket.weekday(), Weekday::Mon);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_month_bucket_end_crossing_year_backward() {
use chrono::{Datelike, Local, Timelike};
let now = Utc.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap();
let bucket_end = TimeUnit::Months.bucket_end(now, 2);
let local_bucket = bucket_end.with_timezone(&Local);
assert_eq!(local_bucket.year(), 2024);
assert_eq!(local_bucket.month(), 11);
assert_eq!(local_bucket.day(), 1);
assert_eq!(local_bucket.hour(), 0);
assert_eq!(local_bucket.minute(), 0);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_days_crossing_year() {
let then = Utc.with_ymd_and_hms(2024, 12, 28, 12, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
let rotations = TimeUnit::Days.num_rotations(then, now);
use chrono::Local;
let then_local = then.with_timezone(&Local).date_naive();
let now_local = now.with_timezone(&Local).date_naive();
let expected = (now_local - then_local).num_days();
assert_eq!(rotations, expected);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_months_crossing_year() {
let then = Utc.with_ymd_and_hms(2024, 10, 15, 12, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 2, 15, 12, 0, 0).unwrap();
let rotations = TimeUnit::Months.num_rotations(then, now);
assert_eq!(rotations, 4);
}
#[test]
#[cfg(feature = "calendar")]
fn test_num_rotations_years_multiple() {
let then = Utc.with_ymd_and_hms(2020, 6, 15, 12, 0, 0).unwrap();
let now = Utc.with_ymd_and_hms(2025, 6, 15, 12, 0, 0).unwrap();
let rotations = TimeUnit::Years.num_rotations(then, now);
assert_eq!(rotations, 5);
}
}