1use crate::enums::named_enum;
4use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike};
5
6named_enum! {
7 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
9 pub enum Unit {
10 Hour => "hour",
12 Day => "day",
14 Week => "week",
16 Month => "month",
18 }
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct Period {
24 pub unit: Unit,
26 pub start: NaiveDateTime,
28}
29
30impl Period {
31 pub fn containing(unit: Unit, at: NaiveDateTime) -> Self {
33 let d = at.date();
34 let day = match unit {
35 Unit::Hour | Unit::Day => d,
36 Unit::Week => d - Duration::days(i64::from(d.weekday().num_days_from_monday())),
37 Unit::Month => d.with_day(1).expect("day 1 exists"),
38 };
39 let hour = if unit == Unit::Hour { at.hour() } else { 0 };
40 Self { unit, start: day.and_hms_opt(hour, 0, 0).expect("valid hour") }
41 }
42
43 pub fn current(unit: Unit) -> Self {
45 Self::containing(unit, Local::now().naive_local())
46 }
47
48 pub fn end(self) -> NaiveDateTime {
50 match self.unit {
51 Unit::Hour => self.start + Duration::hours(1),
52 Unit::Day => self.start + Duration::days(1),
53 Unit::Week => self.start + Duration::days(7),
54 Unit::Month => {
56 let d = self.start.date();
57 let (y, m) = if d.month() == 12 { (d.year() + 1, 1) } else { (d.year(), d.month() + 1) };
58 NaiveDate::from_ymd_opt(y, m, 1).expect("valid month").and_time(NaiveTime::MIN)
59 }
60 }
61 }
62
63 pub fn next(self) -> Self {
65 Self { unit: self.unit, start: self.end() }
66 }
67
68 pub fn prev(self) -> Self {
70 Self::containing(self.unit, self.start - Duration::seconds(1))
71 }
72
73 pub fn contains(self, at: NaiveDateTime) -> bool {
75 self.start <= at && at < self.end()
76 }
77
78 pub fn with_unit(self, unit: Unit) -> Self {
80 let now = Local::now().naive_local();
81 Self::containing(unit, if self.contains(now) { now } else { self.start })
82 }
83
84 pub fn bounds(self) -> (DateTime<Local>, DateTime<Local>) {
86 (to_local(self.start), to_local(self.end()))
87 }
88
89 pub fn label(self) -> String {
91 match self.unit {
92 Unit::Hour => format!(
93 "{} {}–{}",
94 self.start.format("%a %Y-%m-%d"),
95 self.start.format("%H:%M"),
96 self.end().format("%H:%M")
97 ),
98 Unit::Day => self.start.format("%A %Y-%m-%d").to_string(),
99 Unit::Week => format!(
100 "week {} · {} – {}",
101 self.start.iso_week().week(),
102 self.start.format("%b %d"),
103 (self.end() - Duration::days(1)).format("%b %d %Y")
104 ),
105 Unit::Month => self.start.format("%B %Y").to_string(),
106 }
107 }
108
109 pub fn columns(self) -> Vec<(String, Period)> {
112 let sub = match self.unit {
113 Unit::Week => Unit::Day,
114 Unit::Month => Unit::Week,
115 Unit::Hour | Unit::Day => return Vec::new(),
116 };
117 let mut cols = Vec::new();
118 let mut p = Period::containing(sub, self.start);
119 while p.start < self.end() {
120 let label = match sub {
121 Unit::Day => p.start.format("%a %d").to_string(),
122 _ => format!("W{}", p.start.iso_week().week()),
123 };
124 cols.push((label, p));
125 p = p.next();
126 }
127 cols
128 }
129}
130
131fn to_local(naive: NaiveDateTime) -> DateTime<Local> {
134 (0..=4)
135 .find_map(|quarter| Local.from_local_datetime(&(naive + Duration::minutes(15 * quarter))).earliest())
136 .unwrap_or_else(|| Local.from_utc_datetime(&naive))
137}
138
139#[cfg(test)]
140#[path = "../tests/period.rs"]
141mod tests;