1use chrono::{DateTime, Duration, Utc};
2#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4
5use crate::{Error, Result};
6
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub enum TimeUnit {
50 Seconds,
51 Minutes,
52 Hours,
53 Days, Weeks, Months, Years, Ever, }
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct TimeWindow {
68 pub count: usize,
69 pub time_unit: TimeUnit,
70}
71
72impl From<TimeUnit> for TimeWindow {
73 fn from(unit: TimeUnit) -> Self {
74 TimeWindow {
75 count: 1,
76 time_unit: unit,
77 }
78 }
79}
80
81impl From<(usize, TimeUnit)> for TimeWindow {
82 fn from(window: (usize, TimeUnit)) -> Self {
83 TimeWindow {
84 count: window.0,
85 time_unit: window.1,
86 }
87 }
88}
89
90impl From<Duration> for TimeWindow {
91 fn from(dur: Duration) -> Self {
92 let dur = dur.abs();
94
95 if dur.num_days() > 0 {
97 TimeWindow {
98 count: dur.num_days() as usize,
99 time_unit: TimeUnit::Days,
100 }
101 } else if dur.num_hours() > 0 {
102 TimeWindow {
103 count: dur.num_hours() as usize,
104 time_unit: TimeUnit::Hours,
105 }
106 } else if dur.num_minutes() > 0 {
107 TimeWindow {
108 count: dur.num_minutes() as usize,
109 time_unit: TimeUnit::Minutes,
110 }
111 } else {
112 TimeWindow {
113 count: dur.num_seconds() as usize,
114 time_unit: TimeUnit::Minutes,
115 }
116 }
117 }
118}
119
120#[cfg(feature = "calendar")]
122mod calendar {
123 use chrono::{DateTime, Datelike, Local, NaiveDate, TimeZone, Utc};
124
125 pub(super) fn local_midnight_to_utc(date: NaiveDate) -> DateTime<Utc> {
127 Local
128 .from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
129 .earliest()
130 .unwrap()
131 .with_timezone(&Utc)
132 }
133
134 pub(super) fn local_time_to_utc(date: NaiveDate, hour: u32) -> DateTime<Utc> {
136 Local
137 .from_local_datetime(&date.and_hms_opt(hour, 0, 0).unwrap())
138 .earliest()
139 .unwrap()
140 .with_timezone(&Utc)
141 }
142
143 pub(super) fn find_monday(date: NaiveDate) -> NaiveDate {
145 let days_since_monday = date.weekday().num_days_from_monday();
146 date - chrono::Days::new(days_since_monday as u64)
147 }
148
149 pub(super) fn add_months(year: i32, month: u32, delta: i32) -> (i32, u32) {
153 let total_month = month as i32 + delta;
154 let month0 = total_month - 1;
156 let year_offset = month0.div_euclid(12);
157 let final_year = year + year_offset;
158 let final_month = (month0.rem_euclid(12) + 1) as u32;
159 (final_year, final_month)
160 }
161}
162
163impl TimeUnit {
164 pub fn duration(&self) -> Duration {
173 match self {
174 Self::Seconds => Duration::seconds(1),
175 Self::Minutes => Duration::minutes(1),
176 Self::Hours => Duration::hours(1),
177 Self::Days => Duration::days(1),
178 Self::Weeks => Duration::weeks(1),
179 Self::Months => Duration::days(30),
180 Self::Years => Duration::days(365),
181 Self::Ever => {
182 panic!("TimeUnit::Ever has no fixed duration. Resolve to concrete time unit first.")
183 }
184 }
185 }
186
187 pub(crate) fn num_rotations(&self, then: DateTime<Utc>, now: DateTime<Utc>) -> i64 {
201 #[cfg(feature = "calendar")]
202 {
203 use chrono::{Datelike, Local};
204
205 match self {
206 TimeUnit::Days => {
207 let then_local = then.with_timezone(&Local).date_naive();
208 let now_local = now.with_timezone(&Local).date_naive();
209 return (now_local - then_local).num_days();
210 }
211 TimeUnit::Weeks => {
212 let then_local = then.with_timezone(&Local).date_naive();
213 let now_local = now.with_timezone(&Local).date_naive();
214 let week_diff =
215 now_local.iso_week().week() as i64 - then_local.iso_week().week() as i64;
216 let year_diff = (now_local.iso_week().year() as i64
219 - then_local.iso_week().year() as i64)
220 * 52;
221 return week_diff + year_diff;
222 }
223 TimeUnit::Months => {
224 let then_local = then.with_timezone(&Local);
225 let now_local = now.with_timezone(&Local);
226 let then_months = then_local.year() as i64 * 12 + then_local.month() as i64;
227 let now_months = now_local.year() as i64 * 12 + now_local.month() as i64;
228 return now_months - then_months;
229 }
230 TimeUnit::Years => {
231 let then_local = then.with_timezone(&Local);
232 let now_local = now.with_timezone(&Local);
233 return (now_local.year() - then_local.year()) as i64;
234 }
235 _ => {} }
237 }
238
239 let duration = now.signed_duration_since(then);
241 let unit_duration = self.duration();
242 duration.num_seconds() / unit_duration.num_seconds()
243 }
244
245 pub(crate) fn rotate_start_interval(
246 &self,
247 interval_start: DateTime<Utc>,
248 rotations: i64,
249 ) -> DateTime<Utc> {
250 #[cfg(feature = "calendar")]
251 {
252 use chrono::{Datelike, Local, TimeZone};
253
254 match self {
255 TimeUnit::Days => {
256 let local = interval_start.with_timezone(&Local);
257 let target_date = local.date_naive() + chrono::Days::new(rotations as u64);
258 return calendar::local_midnight_to_utc(target_date);
259 }
260 TimeUnit::Weeks => {
261 let local = interval_start.with_timezone(&Local);
262 let this_monday = calendar::find_monday(local.date_naive());
263 let target_monday = this_monday + chrono::Days::new((rotations * 7) as u64);
264 return calendar::local_midnight_to_utc(target_monday);
265 }
266 TimeUnit::Months => {
267 let local = interval_start.with_timezone(&Local);
268 let (final_year, final_month) =
269 calendar::add_months(local.year(), local.month(), rotations as i32);
270 return Local
271 .with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
272 .earliest()
273 .unwrap()
274 .with_timezone(&Utc);
275 }
276 TimeUnit::Years => {
277 let local = interval_start.with_timezone(&Local);
278 let target_year = local.year() + rotations as i32;
279 return Local
280 .with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
281 .earliest()
282 .unwrap()
283 .with_timezone(&Utc);
284 }
285 _ => {} }
287 }
288
289 let duration = self.duration();
291 interval_start + duration * rotations as i32
292 }
293
294 pub(crate) fn bucket_idx(
295 &self,
296 interval_start: DateTime<Utc>,
297 time: DateTime<Utc>,
298 ) -> Result<usize> {
299 let rotations = self.num_rotations(time, interval_start);
300
301 if rotations < 0 {
303 return Err(Error::FutureEvent);
305 }
306 Ok(if rotations == 0 {
307 if time >= interval_start {
309 0 } else {
311 1 }
313 } else {
314 rotations as usize
316 })
317 }
318
319 pub(crate) fn bucket_end(&self, now: DateTime<Utc>, bucket_idx: usize) -> DateTime<Utc> {
324 if bucket_idx == 0 {
325 return now;
326 }
327
328 #[cfg(feature = "calendar")]
329 {
330 use chrono::{Datelike, Local, TimeZone};
331
332 match self {
333 TimeUnit::Days => {
334 let now_local = now.with_timezone(&Local);
335 let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
336 return calendar::local_midnight_to_utc(target_date);
337 }
338 TimeUnit::Weeks => {
339 let now_local = now.with_timezone(&Local);
340 let this_monday = calendar::find_monday(now_local.date_naive());
341 let target_monday = this_monday - chrono::Days::new((bucket_idx * 7) as u64);
342 return calendar::local_midnight_to_utc(target_monday);
343 }
344 TimeUnit::Months => {
345 let now_local = now.with_timezone(&Local);
346 let (final_year, final_month) = calendar::add_months(
347 now_local.year(),
348 now_local.month(),
349 -(bucket_idx as i32),
350 );
351 return Local
352 .with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
353 .earliest()
354 .unwrap()
355 .with_timezone(&Utc);
356 }
357 TimeUnit::Years => {
358 let now_local = now.with_timezone(&Local);
359 let target_year = now_local.year() - bucket_idx as i32;
360 return Local
361 .with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
362 .earliest()
363 .unwrap()
364 .with_timezone(&Utc);
365 }
366 _ => {} }
368 }
369
370 now - (self.duration() * bucket_idx as i32)
372 }
373
374 pub(crate) fn bucket_start(&self, now: DateTime<Utc>, bucket_idx: usize) -> DateTime<Utc> {
376 self.bucket_end(now, bucket_idx + 1)
377 }
378
379 #[allow(dead_code)] pub(crate) fn bucket_duration(&self, now: DateTime<Utc>, bucket_idx: usize) -> Duration {
382 let bucket_start = self.bucket_start(now, bucket_idx);
383 let bucket_end = self.bucket_end(now, bucket_idx);
384 bucket_end - bucket_start
385 }
386
387 pub(crate) fn bucket_midway(
389 &self,
390 clock_now: DateTime<Utc>,
391 interval_start: DateTime<Utc>,
392 bucket_idx: usize,
393 ) -> DateTime<Utc> {
394 if bucket_idx == 0 {
395 let elapsed = clock_now - interval_start;
396 return interval_start + (elapsed / 2);
397 }
398
399 #[cfg(feature = "calendar")]
400 {
401 use chrono::Local;
402
403 match self {
404 TimeUnit::Days => {
405 let now_local = clock_now.with_timezone(&Local);
406 let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
407 return calendar::local_time_to_utc(target_date, 12);
408 }
409 TimeUnit::Weeks | TimeUnit::Months | TimeUnit::Years => {
410 let bucket_start = self.bucket_start(clock_now, bucket_idx);
411 let duration = self.bucket_duration(clock_now, bucket_idx);
412 return bucket_start + (duration / 2);
413 }
414 _ => {} }
416 }
417
418 let duration = self.duration();
420 interval_start - (duration * bucket_idx as i32 + duration / 2)
421 }
422
423 pub(crate) fn first_moment_ever(
425 &self,
426 now: DateTime<Utc>,
427 bucket_count: usize,
428 ) -> DateTime<Utc> {
429 #[cfg(feature = "calendar")]
430 {
431 use chrono::{Datelike, Local, TimeZone};
432
433 match self {
434 TimeUnit::Days => {
435 let now_local = now.with_timezone(&Local);
436 let target_date =
437 now_local.date_naive() - chrono::Days::new(bucket_count as u64);
438 return calendar::local_midnight_to_utc(target_date);
439 }
440 TimeUnit::Weeks => {
441 let now_local = now.with_timezone(&Local);
442 let this_monday = calendar::find_monday(now_local.date_naive());
443 let target_monday = this_monday - chrono::Days::new((bucket_count * 7) as u64);
444 return calendar::local_midnight_to_utc(target_monday);
445 }
446 TimeUnit::Months => {
447 let now_local = now.with_timezone(&Local);
448 let (final_year, final_month) = calendar::add_months(
449 now_local.year(),
450 now_local.month(),
451 -(bucket_count as i32),
452 );
453 return Local
454 .with_ymd_and_hms(final_year, final_month, 1, 0, 0, 0)
455 .earliest()
456 .unwrap()
457 .with_timezone(&Utc);
458 }
459 TimeUnit::Years => {
460 let now_local = now.with_timezone(&Local);
461 let target_year = now_local.year() - bucket_count as i32;
462 return Local
463 .with_ymd_and_hms(target_year, 1, 1, 0, 0, 0)
464 .earliest()
465 .unwrap()
466 .with_timezone(&Utc);
467 }
468 _ => {} }
470 }
471
472 now - (self.duration() * bucket_count as i32)
474 }
475
476 pub(crate) fn bucket_time(
480 &self,
481 clock_now: DateTime<Utc>,
482 interval_start: DateTime<Utc>,
483 bucket_idx: usize,
484 ) -> DateTime<Utc> {
485 if bucket_idx == 0 {
486 let elapsed = clock_now - interval_start;
487 return interval_start + (elapsed / 2);
488 }
489
490 #[cfg(feature = "calendar")]
491 {
492 use chrono::Local;
493
494 match self {
495 TimeUnit::Days => {
496 let now_local = clock_now.with_timezone(&Local);
497 let target_date = now_local.date_naive() - chrono::Days::new(bucket_idx as u64);
498 return calendar::local_time_to_utc(target_date, 12);
499 }
500 TimeUnit::Weeks | TimeUnit::Months | TimeUnit::Years => {
501 let bucket_start = self.bucket_start(clock_now, bucket_idx);
502 let duration = self.bucket_duration(clock_now, bucket_idx);
503 return bucket_start + (duration / 2);
504 }
505 _ => {} }
507 }
508
509 let duration = self.duration();
511 interval_start - (duration * bucket_idx as i32) + duration / 2
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use chrono::TimeZone;
519
520 #[cfg(feature = "calendar")]
521 mod calendar_helpers {
522 use super::*;
523 use chrono::Local;
524
525 pub fn calendar_days_between(then: DateTime<Utc>, now: DateTime<Utc>) -> i64 {
528 let then_local = then.with_timezone(&Local).date_naive();
530 let now_local = now.with_timezone(&Local).date_naive();
531
532 (now_local - then_local).num_days()
534 }
535 }
536
537 #[test]
538 fn test_duration_minutes() {
539 assert_eq!(TimeUnit::Minutes.duration(), Duration::minutes(1));
540 }
541
542 #[test]
543 fn test_duration_hours() {
544 assert_eq!(TimeUnit::Hours.duration(), Duration::hours(1));
545 }
546
547 #[test]
548 fn test_duration_days() {
549 assert_eq!(TimeUnit::Days.duration(), Duration::days(1));
552 }
553
554 #[test]
555 fn test_duration_weeks() {
556 assert_eq!(TimeUnit::Weeks.duration(), Duration::weeks(1));
557 }
558
559 #[test]
560 fn test_duration_months() {
561 assert_eq!(TimeUnit::Months.duration(), Duration::days(30));
564 }
565
566 #[test]
567 fn test_duration_years() {
568 assert_eq!(TimeUnit::Years.duration(), Duration::days(365));
570 }
571
572 #[test]
573 fn test_num_rotations_same_time() {
574 let time = Utc::now();
575 assert_eq!(TimeUnit::Days.num_rotations(time, time), 0);
576 }
577
578 #[test]
579 fn test_num_rotations_past_time() {
580 let now = Utc.with_ymd_and_hms(2025, 1, 10, 12, 0, 0).unwrap();
581 let then = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
582
583 assert_eq!(TimeUnit::Days.num_rotations(then, now), 5);
585 }
586
587 #[test]
588 fn test_num_rotations_future_time() {
589 let now = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
590 let future = Utc.with_ymd_and_hms(2025, 1, 10, 12, 0, 0).unwrap();
591
592 assert_eq!(TimeUnit::Days.num_rotations(future, now), -5);
594 }
595
596 #[test]
597 fn test_num_rotations_hours() {
598 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap();
599 let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
600
601 assert_eq!(TimeUnit::Hours.num_rotations(then, now), 5);
603 }
604
605 #[test]
606 fn test_num_rotations_seconds() {
607 let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 45).unwrap();
608 let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
609
610 assert_eq!(TimeUnit::Seconds.num_rotations(then, now), 45);
612 }
613
614 #[test]
615 fn test_num_rotations_minutes() {
616 let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 30, 0).unwrap();
617 let then = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
618
619 assert_eq!(TimeUnit::Minutes.num_rotations(then, now), 30);
621 }
622
623 #[test]
624 fn test_num_rotations_weeks() {
625 let now = Utc.with_ymd_and_hms(2025, 1, 22, 0, 0, 0).unwrap();
626 let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
627
628 assert_eq!(TimeUnit::Weeks.num_rotations(then, now), 3);
630 }
631
632 #[test]
633 #[cfg(not(feature = "calendar"))]
634 fn test_num_rotations_months() {
635 let now = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
636 let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
637
638 let days_diff = (now - then).num_days();
640 assert_eq!(days_diff, 90);
641 assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
642 }
643
644 #[test]
645 #[cfg(feature = "calendar")]
646 fn test_num_rotations_months_calendar_year_boundary() {
647 let then = Utc.with_ymd_and_hms(2024, 12, 31, 12, 0, 0).unwrap();
650 let now = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
651
652 let rotations = TimeUnit::Months.num_rotations(then, now);
653
654 assert_eq!(
660 rotations, 3,
661 "Should be 3 months from Dec 31, 2024 to March 1, 2025"
662 );
663 }
664
665 #[test]
666 #[cfg(feature = "calendar")]
667 fn test_num_rotations_months_calendar_multi_year_span() {
668 let then = Utc.with_ymd_and_hms(2023, 12, 15, 0, 0, 0).unwrap();
671 let now = Utc.with_ymd_and_hms(2025, 12, 1, 0, 0, 0).unwrap();
672
673 let rotations = TimeUnit::Months.num_rotations(then, now);
674
675 assert_eq!(
680 rotations, 24,
681 "Should be 24 months from Dec 2023 to Dec 2025"
682 );
683 }
684
685 #[test]
686 #[cfg(feature = "calendar")]
687 fn test_num_rotations_months_calendar() {
688 let now = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
689 let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
690
691 assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
693
694 let now = Utc.with_ymd_and_hms(2025, 4, 15, 12, 0, 0).unwrap();
696 let then = Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
697
698 assert_eq!(TimeUnit::Months.num_rotations(then, now), 3);
700 }
701
702 #[test]
703 #[cfg(feature = "calendar")]
704 fn test_num_rotations_weeks_calendar_year_boundary() {
705 use chrono::Datelike;
706
707 let then = Utc.with_ymd_and_hms(2024, 12, 31, 12, 0, 0).unwrap();
712 let now = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
713
714 use chrono::Local;
716 let then_local = then.with_timezone(&Local).date_naive();
717 let now_local = now.with_timezone(&Local).date_naive();
718
719 assert_eq!(
721 then_local.iso_week().year(),
722 2025,
723 "Dec 31, 2024 should be in ISO week year 2025"
724 );
725 assert_eq!(
726 then_local.iso_week().week(),
727 1,
728 "Dec 31, 2024 should be in ISO week 1"
729 );
730
731 assert_eq!(
733 now_local.iso_week().week(),
734 9,
735 "March 1, 2025 should be in ISO week 9"
736 );
737
738 let rotations = TimeUnit::Weeks.num_rotations(then, now);
739
740 assert_eq!(
751 rotations, 8,
752 "Should be 8 ISO weeks from Dec 31, 2024 to March 1, 2025"
753 );
754 }
755
756 #[test]
757 #[cfg(feature = "calendar")]
758 fn test_num_rotations_weeks_calendar_multi_year_span() {
759 use chrono::Datelike;
760
761 let then = Utc.with_ymd_and_hms(2024, 12, 30, 0, 0, 0).unwrap();
766 let now = Utc.with_ymd_and_hms(2025, 12, 29, 0, 0, 0).unwrap();
767
768 use chrono::Local;
769 let then_local = then.with_timezone(&Local).date_naive();
770 let now_local = now.with_timezone(&Local).date_naive();
771
772 assert_eq!(
774 then_local.iso_week().year(),
775 2025,
776 "Dec 30, 2024 should be in ISO week year 2025"
777 );
778 assert_eq!(
779 then_local.iso_week().week(),
780 1,
781 "Dec 30, 2024 should be in ISO week 1"
782 );
783
784 assert_eq!(
785 now_local.iso_week().year(),
786 2026,
787 "Dec 29, 2025 should be in ISO week year 2026"
788 );
789 assert_eq!(
790 now_local.iso_week().week(),
791 1,
792 "Dec 29, 2025 should be in ISO week 1"
793 );
794
795 let rotations = TimeUnit::Weeks.num_rotations(then, now);
796
797 assert_eq!(
808 rotations, 52,
809 "Should be 52 ISO weeks from Dec 30, 2024 to Dec 29, 2025"
810 );
811 }
812
813 #[test]
814 #[cfg(feature = "calendar")]
815 fn test_num_rotations_days_calendar() {
816 use calendar_helpers::calendar_days_between;
817
818 let then = Utc.with_ymd_and_hms(2025, 1, 1, 23, 0, 0).unwrap();
823 let now = Utc.with_ymd_and_hms(2025, 1, 5, 1, 0, 0).unwrap();
824
825 let expected_days = calendar_days_between(then, now);
827
828 let rotations = TimeUnit::Days.num_rotations(then, now);
829
830 assert_eq!(
832 rotations, expected_days,
833 "Expected {} calendar days (local timezone), got {}",
834 expected_days, rotations
835 );
836 }
837
838 #[test]
839 fn test_num_rotations_years() {
840 let now = Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap();
841 let then = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
842
843 assert_eq!(TimeUnit::Years.num_rotations(then, now), 2);
845 }
846
847 #[test]
848 fn test_num_rotations_partial_unit() {
849 let now = Utc.with_ymd_and_hms(2025, 1, 1, 12, 30, 0).unwrap();
850 let then = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
851
852 assert_eq!(TimeUnit::Hours.num_rotations(then, now), 0);
854 }
855
856 #[test]
858 fn test_num_rotations_transitive() {
859 let a = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
860 let b = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
861 let c = Utc.with_ymd_and_hms(2025, 1, 10, 0, 0, 0).unwrap();
862
863 let ab = TimeUnit::Days.num_rotations(a, b);
864 let bc = TimeUnit::Days.num_rotations(b, c);
865 let ac = TimeUnit::Days.num_rotations(a, c);
866
867 assert_eq!(ab + bc, ac);
868 }
869
870 #[test]
871 fn test_time_unit_is_copy() {
872 let unit = TimeUnit::Days;
873 let _unit2 = unit; let _unit3 = unit; }
876
877 #[test]
878 fn test_time_unit_is_eq() {
879 assert_eq!(TimeUnit::Days, TimeUnit::Days);
880 assert_ne!(TimeUnit::Days, TimeUnit::Hours);
881 }
882
883 #[test]
884 fn test_time_unit_is_hash() {
885 use std::collections::HashSet;
886 let mut set = HashSet::new();
887 set.insert(TimeUnit::Days);
888 assert!(set.contains(&TimeUnit::Days));
889 }
890
891 #[test]
892 fn test_large_time_jump() {
893 let now = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
894 let then = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
895
896 let rotations = TimeUnit::Days.num_rotations(then, now);
898 assert!(rotations > 2000); }
900
901 #[test]
903 fn test_time_window_from_time_unit() {
904 let window: TimeWindow = TimeUnit::Days.into();
905 assert_eq!(window.count, 1);
906 assert_eq!(window.time_unit, TimeUnit::Days);
907 }
908
909 #[test]
910 fn test_time_window_from_tuple() {
911 let window: TimeWindow = (7, TimeUnit::Days).into();
912 assert_eq!(window.count, 7);
913 assert_eq!(window.time_unit, TimeUnit::Days);
914 }
915
916 #[test]
917 fn test_time_window_from_duration_days() {
918 let window: TimeWindow = Duration::days(7).into();
919 assert_eq!(window.count, 7);
920 assert_eq!(window.time_unit, TimeUnit::Days);
921 }
922
923 #[test]
924 fn test_time_window_from_duration_hours() {
925 let window: TimeWindow = Duration::hours(12).into();
926 assert_eq!(window.count, 12);
927 assert_eq!(window.time_unit, TimeUnit::Hours);
928 }
929
930 #[test]
931 fn test_time_window_from_duration_minutes() {
932 let window: TimeWindow = Duration::minutes(30).into();
933 assert_eq!(window.count, 30);
934 assert_eq!(window.time_unit, TimeUnit::Minutes);
935 }
936
937 #[test]
938 fn test_time_window_from_duration_seconds() {
939 let window: TimeWindow = Duration::seconds(45).into();
940 assert_eq!(window.count, 45);
942 assert_eq!(window.time_unit, TimeUnit::Minutes);
943 }
944
945 #[test]
947 fn test_time_unit_ord() {
948 assert!(TimeUnit::Minutes < TimeUnit::Hours);
949 assert!(TimeUnit::Hours < TimeUnit::Days);
950 assert!(TimeUnit::Days < TimeUnit::Weeks);
951 assert!(TimeUnit::Weeks < TimeUnit::Months);
952 assert!(TimeUnit::Months < TimeUnit::Years);
953 }
954
955 #[test]
956 fn test_time_unit_ord_transitive() {
957 assert!(TimeUnit::Minutes < TimeUnit::Days);
958 assert!(TimeUnit::Hours < TimeUnit::Weeks);
959 assert!(TimeUnit::Days < TimeUnit::Years);
960 }
961
962 #[test]
963 fn test_time_unit_ord_reflexive() {
964 assert!(TimeUnit::Minutes <= TimeUnit::Minutes);
965 assert!(TimeUnit::Hours >= TimeUnit::Hours);
966 }
967
968 #[test]
969 fn test_time_unit_ever_is_largest() {
970 assert!(TimeUnit::Years < TimeUnit::Ever);
971 assert!(TimeUnit::Minutes < TimeUnit::Ever);
972 assert!(TimeUnit::Days < TimeUnit::Ever);
973 }
974
975 #[test]
976 #[should_panic(expected = "TimeUnit::Ever has no fixed duration")]
977 fn test_time_unit_ever_duration_panics() {
978 TimeUnit::Ever.duration();
979 }
980
981 #[test]
982 fn test_time_window_from_negative_duration_days() {
983 let window: TimeWindow = Duration::days(-7).into();
984 assert_eq!(window.count, 7);
986 assert_eq!(window.time_unit, TimeUnit::Days);
987 }
988
989 #[test]
990 fn test_time_window_from_negative_duration_hours() {
991 let window: TimeWindow = Duration::hours(-12).into();
992 assert_eq!(window.count, 12);
993 assert_eq!(window.time_unit, TimeUnit::Hours);
994 }
995
996 #[test]
997 fn test_time_window_from_negative_duration_minutes() {
998 let window: TimeWindow = Duration::minutes(-30).into();
999 assert_eq!(window.count, 30);
1000 assert_eq!(window.time_unit, TimeUnit::Minutes);
1001 }
1002
1003 #[test]
1004 fn test_time_window_from_negative_duration_seconds() {
1005 let window: TimeWindow = Duration::seconds(-45).into();
1006 assert_eq!(window.count, 45);
1007 assert_eq!(window.time_unit, TimeUnit::Minutes);
1008 }
1009
1010 #[test]
1011 #[cfg(feature = "calendar")]
1012 fn test_bucket_end_days_calendar() {
1013 use chrono::{Local, Timelike};
1014
1015 let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
1016
1017 assert_eq!(TimeUnit::Days.bucket_end(now, 0), now);
1019
1020 let bucket_end = TimeUnit::Days.bucket_end(now, 1);
1022 let local_bucket = bucket_end.with_timezone(&Local);
1023
1024 assert_eq!(local_bucket.hour(), 0);
1026 assert_eq!(local_bucket.minute(), 0);
1027 assert_eq!(local_bucket.second(), 0);
1028 }
1029
1030 #[test]
1031 #[cfg(feature = "calendar")]
1032 fn test_bucket_end_months_calendar() {
1033 use chrono::{Datelike, Local, Timelike};
1034
1035 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1036
1037 assert_eq!(TimeUnit::Months.bucket_end(now, 0), now);
1039
1040 let bucket_end = TimeUnit::Months.bucket_end(now, 1);
1042 let local_bucket = bucket_end.with_timezone(&Local);
1043
1044 assert_eq!(local_bucket.month(), 3);
1046 assert_eq!(local_bucket.day(), 1);
1047 assert_eq!(local_bucket.hour(), 0);
1048 assert_eq!(local_bucket.minute(), 0);
1049 assert_eq!(local_bucket.second(), 0);
1050 }
1051
1052 #[test]
1053 #[cfg(feature = "calendar")]
1054 fn test_bucket_start_calendar() {
1055 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1056
1057 let bucket_0_start = TimeUnit::Months.bucket_start(now, 0);
1059 let bucket_1_end = TimeUnit::Months.bucket_end(now, 1);
1060
1061 assert_eq!(bucket_0_start, bucket_1_end);
1062 }
1063
1064 #[test]
1065 #[cfg(feature = "calendar")]
1066 fn test_bucket_start_months_calendar() {
1067 use chrono::{Datelike, Local, Timelike};
1068
1069 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1070
1071 let bucket_1_start = TimeUnit::Months.bucket_start(now, 1);
1073 let local_start = bucket_1_start.with_timezone(&Local);
1074
1075 assert_eq!(local_start.month(), 2);
1077 assert_eq!(local_start.day(), 1);
1078 assert_eq!(local_start.hour(), 0);
1079 assert_eq!(local_start.minute(), 0);
1080 assert_eq!(local_start.second(), 0);
1081
1082 let bucket_2_end = TimeUnit::Months.bucket_end(now, 2);
1084 assert_eq!(bucket_1_start, bucket_2_end);
1085 }
1086
1087 #[test]
1088 #[cfg(feature = "calendar")]
1089 fn test_rotate_start_interval_days_calendar() {
1090 use chrono::{Datelike, Local, Timelike};
1091
1092 let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1093
1094 let rotated = TimeUnit::Days.rotate_start_interval(start, 3);
1096 let local_rotated = rotated.with_timezone(&Local);
1097
1098 assert_eq!(local_rotated.day(), 4);
1100 assert_eq!(local_rotated.hour(), 0);
1101 assert_eq!(local_rotated.minute(), 0);
1102 }
1103
1104 #[test]
1105 #[cfg(feature = "calendar")]
1106 fn test_rotate_start_interval_months_calendar() {
1107 use chrono::{Datelike, Local, Timelike};
1108
1109 let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1110
1111 let rotated = TimeUnit::Months.rotate_start_interval(start, 3);
1113 let local_rotated = rotated.with_timezone(&Local);
1114
1115 assert_eq!(local_rotated.month(), 4);
1117 assert_eq!(local_rotated.day(), 1);
1118 assert_eq!(local_rotated.hour(), 0);
1119 assert_eq!(local_rotated.minute(), 0);
1120
1121 let start = Utc.with_ymd_and_hms(2025, 11, 1, 0, 0, 0).unwrap();
1123 let rotated = TimeUnit::Months.rotate_start_interval(start, 3);
1124 let local_rotated = rotated.with_timezone(&Local);
1125
1126 assert_eq!(local_rotated.year(), 2026);
1128 assert_eq!(local_rotated.month(), 2);
1129 assert_eq!(local_rotated.day(), 1);
1130 }
1131
1132 #[test]
1133 #[cfg(feature = "calendar")]
1134 fn test_bucket_midway_days_calendar() {
1135 use chrono::{Local, Timelike};
1136
1137 let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
1138 let interval_start = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
1139
1140 let midway_0 = TimeUnit::Days.bucket_midway(now, interval_start, 0);
1142 let expected_0 = interval_start + (now - interval_start) / 2;
1143 assert_eq!(midway_0, expected_0);
1144
1145 let midway_1 = TimeUnit::Days.bucket_midway(now, interval_start, 1);
1147 let local_midway = midway_1.with_timezone(&Local);
1148
1149 assert_eq!(local_midway.hour(), 12);
1151 assert_eq!(local_midway.minute(), 0);
1152 assert_eq!(local_midway.second(), 0);
1153 }
1154
1155 #[test]
1156 #[cfg(feature = "calendar")]
1157 fn test_bucket_midway_months_calendar() {
1158 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1159 let interval_start = Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
1160
1161 let midway_1 = TimeUnit::Months.bucket_midway(now, interval_start, 1);
1163
1164 let bucket_start = TimeUnit::Months.bucket_start(now, 1);
1166 let bucket_end = TimeUnit::Months.bucket_end(now, 1);
1167 let expected = bucket_start + (bucket_end - bucket_start) / 2;
1168
1169 assert_eq!(midway_1, expected);
1170 }
1171
1172 #[test]
1173 #[cfg(feature = "calendar")]
1174 fn test_first_moment_ever_days_calendar() {
1175 use chrono::{Datelike, Local, Timelike};
1176
1177 let now = Utc.with_ymd_and_hms(2025, 1, 10, 14, 30, 0).unwrap();
1178
1179 let first = TimeUnit::Days.first_moment_ever(now, 5);
1181 let local_first = first.with_timezone(&Local);
1182
1183 let now_local = now.with_timezone(&Local);
1185 assert_eq!(local_first.day(), now_local.day() - 5);
1186 assert_eq!(local_first.hour(), 0);
1187 assert_eq!(local_first.minute(), 0);
1188 assert_eq!(local_first.second(), 0);
1189 }
1190
1191 #[test]
1192 #[cfg(feature = "calendar")]
1193 fn test_first_moment_ever_months_calendar() {
1194 use chrono::{Datelike, Local, Timelike};
1195
1196 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1197
1198 let first = TimeUnit::Months.first_moment_ever(now, 3);
1200 let local_first = first.with_timezone(&Local);
1201
1202 assert_eq!(local_first.month(), 1);
1204 assert_eq!(local_first.day(), 1);
1205 assert_eq!(local_first.hour(), 0);
1206 assert_eq!(local_first.minute(), 0);
1207 }
1208
1209 #[test]
1210 #[cfg(feature = "calendar")]
1211 fn test_bucket_time_days_calendar() {
1212 use chrono::{Local, Timelike};
1213
1214 let now = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
1215 let interval_start = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
1216
1217 let bucket_time_1 = TimeUnit::Days.bucket_time(now, interval_start, 1);
1219 let local_time = bucket_time_1.with_timezone(&Local);
1220
1221 assert_eq!(local_time.hour(), 12);
1223 assert_eq!(local_time.minute(), 0);
1224 assert_eq!(local_time.second(), 0);
1225 }
1226
1227 #[test]
1232 #[cfg(feature = "calendar")]
1233 fn test_bucket_end_weeks_calendar() {
1234 use chrono::{Datelike, Local, Timelike, Weekday};
1235
1236 let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
1238
1239 assert_eq!(TimeUnit::Weeks.bucket_end(now, 0), now);
1241
1242 let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
1244 let local_bucket = bucket_end.with_timezone(&Local);
1245
1246 assert_eq!(local_bucket.weekday(), Weekday::Mon);
1248 assert_eq!(local_bucket.hour(), 0);
1249 assert_eq!(local_bucket.minute(), 0);
1250 assert_eq!(local_bucket.second(), 0);
1251
1252 let now_local = now.with_timezone(&Local);
1254 let this_monday = calendar::find_monday(now_local.date_naive());
1255 let expected_monday = this_monday - chrono::Days::new(7);
1256
1257 assert_eq!(local_bucket.date_naive(), expected_monday);
1258 }
1259
1260 #[test]
1261 #[cfg(feature = "calendar")]
1262 fn test_bucket_midway_weeks_calendar() {
1263 let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
1265 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);
1269
1270 let bucket_start = TimeUnit::Weeks.bucket_start(now, 1);
1272 let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
1273 let expected = bucket_start + (bucket_end - bucket_start) / 2;
1274
1275 assert_eq!(
1276 midway_1, expected,
1277 "Week midpoint should be bucket_start + duration/2"
1278 );
1279 }
1280
1281 #[test]
1282 #[cfg(feature = "calendar")]
1283 fn test_rotate_start_interval_weeks_calendar() {
1284 use chrono::{Datelike, Local, Timelike, Weekday};
1285
1286 let start = Utc.with_ymd_and_hms(2025, 1, 6, 0, 0, 0).unwrap();
1288
1289 let rotated = TimeUnit::Weeks.rotate_start_interval(start, 2);
1291 let local_rotated = rotated.with_timezone(&Local);
1292
1293 assert_eq!(local_rotated.weekday(), Weekday::Mon);
1295 assert_eq!(local_rotated.day(), 20);
1296 assert_eq!(local_rotated.hour(), 0);
1297 assert_eq!(local_rotated.minute(), 0);
1298 }
1299
1300 #[test]
1301 #[cfg(feature = "calendar")]
1302 fn test_first_moment_ever_weeks_calendar() {
1303 use chrono::{Datelike, Local, Timelike, Weekday};
1304
1305 let now = Utc.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap();
1306
1307 let first = TimeUnit::Weeks.first_moment_ever(now, 3);
1309 let local_first = first.with_timezone(&Local);
1310
1311 assert_eq!(local_first.weekday(), Weekday::Mon);
1313 assert_eq!(local_first.hour(), 0);
1314 assert_eq!(local_first.minute(), 0);
1315
1316 let now_local = now.with_timezone(&Local);
1318 let this_monday = calendar::find_monday(now_local.date_naive());
1319 let expected_monday = this_monday - chrono::Days::new(3 * 7);
1320
1321 assert_eq!(local_first.date_naive(), expected_monday);
1322 }
1323
1324 #[test]
1329 #[cfg(feature = "calendar")]
1330 fn test_bucket_end_years_calendar() {
1331 use chrono::{Datelike, Local, Timelike};
1332
1333 let now = Utc.with_ymd_and_hms(2025, 4, 15, 14, 30, 0).unwrap();
1334
1335 assert_eq!(TimeUnit::Years.bucket_end(now, 0), now);
1337
1338 let bucket_end = TimeUnit::Years.bucket_end(now, 1);
1340 let local_bucket = bucket_end.with_timezone(&Local);
1341
1342 assert_eq!(local_bucket.year(), 2024);
1344 assert_eq!(local_bucket.month(), 1);
1345 assert_eq!(local_bucket.day(), 1);
1346 assert_eq!(local_bucket.hour(), 0);
1347 assert_eq!(local_bucket.minute(), 0);
1348 assert_eq!(local_bucket.second(), 0);
1349 }
1350
1351 #[test]
1352 #[cfg(feature = "calendar")]
1353 fn test_bucket_midway_years_calendar() {
1354 let now = Utc.with_ymd_and_hms(2025, 6, 15, 14, 30, 0).unwrap();
1355 let interval_start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1356
1357 let midway_1 = TimeUnit::Years.bucket_midway(now, interval_start, 1);
1359
1360 let bucket_start = TimeUnit::Years.bucket_start(now, 1);
1362 let bucket_end = TimeUnit::Years.bucket_end(now, 1);
1363 let expected = bucket_start + (bucket_end - bucket_start) / 2;
1364
1365 assert_eq!(
1366 midway_1, expected,
1367 "Year midpoint should be bucket_start + duration/2"
1368 );
1369 }
1370
1371 #[test]
1372 #[cfg(feature = "calendar")]
1373 fn test_rotate_start_interval_years_calendar() {
1374 use chrono::{Datelike, Local, Timelike};
1375
1376 let start = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
1377
1378 let rotated = TimeUnit::Years.rotate_start_interval(start, 2);
1380 let local_rotated = rotated.with_timezone(&Local);
1381
1382 assert_eq!(local_rotated.year(), 2025);
1384 assert_eq!(local_rotated.month(), 1);
1385 assert_eq!(local_rotated.day(), 1);
1386 assert_eq!(local_rotated.hour(), 0);
1387 assert_eq!(local_rotated.minute(), 0);
1388 }
1389
1390 #[test]
1391 #[cfg(feature = "calendar")]
1392 fn test_first_moment_ever_years_calendar() {
1393 use chrono::{Datelike, Local, Timelike};
1394
1395 let now = Utc.with_ymd_and_hms(2025, 6, 15, 14, 30, 0).unwrap();
1396
1397 let first = TimeUnit::Years.first_moment_ever(now, 3);
1399 let local_first = first.with_timezone(&Local);
1400
1401 assert_eq!(local_first.year(), 2022);
1403 assert_eq!(local_first.month(), 1);
1404 assert_eq!(local_first.day(), 1);
1405 assert_eq!(local_first.hour(), 0);
1406 assert_eq!(local_first.minute(), 0);
1407 }
1408
1409 #[test]
1414 #[cfg(feature = "calendar")]
1415 fn test_week_spanning_year_boundary() {
1416 use chrono::{Datelike, Local, Timelike, Weekday};
1417
1418 let now = Utc.with_ymd_and_hms(2025, 1, 3, 10, 0, 0).unwrap(); let bucket_end = TimeUnit::Weeks.bucket_end(now, 1);
1423 let local_bucket = bucket_end.with_timezone(&Local);
1424
1425 assert_eq!(local_bucket.year(), 2024);
1426 assert_eq!(local_bucket.month(), 12);
1427 assert_eq!(local_bucket.day(), 23);
1428 assert_eq!(local_bucket.weekday(), Weekday::Mon);
1429 assert_eq!(local_bucket.hour(), 0);
1430 assert_eq!(local_bucket.minute(), 0);
1431 }
1432
1433 #[test]
1434 #[cfg(feature = "calendar")]
1435 fn test_month_bucket_end_crossing_year_backward() {
1436 use chrono::{Datelike, Local, Timelike};
1437
1438 let now = Utc.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap();
1440
1441 let bucket_end = TimeUnit::Months.bucket_end(now, 2);
1443 let local_bucket = bucket_end.with_timezone(&Local);
1444
1445 assert_eq!(local_bucket.year(), 2024);
1446 assert_eq!(local_bucket.month(), 11);
1447 assert_eq!(local_bucket.day(), 1);
1448 assert_eq!(local_bucket.hour(), 0);
1449 assert_eq!(local_bucket.minute(), 0);
1450 }
1451
1452 #[test]
1453 #[cfg(feature = "calendar")]
1454 fn test_num_rotations_days_crossing_year() {
1455 let then = Utc.with_ymd_and_hms(2024, 12, 28, 12, 0, 0).unwrap();
1457 let now = Utc.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
1458
1459 let rotations = TimeUnit::Days.num_rotations(then, now);
1460
1461 use chrono::Local;
1463 let then_local = then.with_timezone(&Local).date_naive();
1464 let now_local = now.with_timezone(&Local).date_naive();
1465 let expected = (now_local - then_local).num_days();
1466
1467 assert_eq!(rotations, expected);
1468 }
1470
1471 #[test]
1472 #[cfg(feature = "calendar")]
1473 fn test_num_rotations_months_crossing_year() {
1474 let then = Utc.with_ymd_and_hms(2024, 10, 15, 12, 0, 0).unwrap();
1476 let now = Utc.with_ymd_and_hms(2025, 2, 15, 12, 0, 0).unwrap();
1477
1478 let rotations = TimeUnit::Months.num_rotations(then, now);
1479
1480 assert_eq!(rotations, 4);
1482 }
1483
1484 #[test]
1485 #[cfg(feature = "calendar")]
1486 fn test_num_rotations_years_multiple() {
1487 let then = Utc.with_ymd_and_hms(2020, 6, 15, 12, 0, 0).unwrap();
1489 let now = Utc.with_ymd_and_hms(2025, 6, 15, 12, 0, 0).unwrap();
1490
1491 let rotations = TimeUnit::Years.num_rotations(then, now);
1492
1493 assert_eq!(rotations, 5);
1495 }
1496}