Skip to main content

devcap_core/
period.rs

1use chrono::{DateTime, Datelike, Duration, Local, NaiveTime};
2use std::str::FromStr;
3
4#[derive(Debug, Clone)]
5pub enum Period {
6    Today,
7    Yesterday,
8    Hours(u32),
9    Days(u32),
10    Week,
11}
12
13pub struct TimeRange {
14    pub since: DateTime<Local>,
15    pub until: Option<DateTime<Local>>,
16}
17
18impl FromStr for Period {
19    type Err = String;
20
21    fn from_str(s: &str) -> Result<Self, Self::Err> {
22        match s {
23            "today" => Ok(Period::Today),
24            "yesterday" => Ok(Period::Yesterday),
25            "week" => Ok(Period::Week),
26            other => {
27                if let Some(h) = other.strip_suffix('h') {
28                    h.parse::<u32>()
29                        .map(Period::Hours)
30                        .map_err(|_| format!("Invalid hours: {other}"))
31                } else if let Some(d) = other.strip_suffix('d') {
32                    d.parse::<u32>()
33                        .map(Period::Days)
34                        .map_err(|_| format!("Invalid days: {other}"))
35                } else {
36                    Err(format!(
37                        "Unknown period: {other}. Use: today, yesterday, 24h, 3d, 7d, week"
38                    ))
39                }
40            }
41        }
42    }
43}
44
45impl Period {
46    pub fn to_time_range(&self) -> TimeRange {
47        let now = Local::now();
48        let start_of_today = now
49            .date_naive()
50            .and_time(NaiveTime::MIN)
51            .and_local_timezone(Local)
52            .single()
53            .unwrap_or(now);
54
55        match self {
56            Period::Today => TimeRange {
57                since: start_of_today,
58                until: None,
59            },
60            Period::Yesterday => {
61                let yesterday_start = start_of_today - Duration::days(1);
62                TimeRange {
63                    since: yesterday_start,
64                    until: Some(start_of_today),
65                }
66            }
67            Period::Hours(h) => TimeRange {
68                since: now - Duration::hours(i64::from(*h)),
69                until: None,
70            },
71            Period::Days(d) => TimeRange {
72                since: now - Duration::days(i64::from(*d)),
73                until: None,
74            },
75            Period::Week => {
76                let days_since_monday = now.weekday().num_days_from_monday() as i64;
77                let monday = start_of_today - Duration::days(days_since_monday);
78                TimeRange {
79                    since: monday,
80                    until: None,
81                }
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use chrono::{Timelike, Weekday};
91
92    #[test]
93    fn parse_today() {
94        assert!(matches!(Period::from_str("today"), Ok(Period::Today)));
95    }
96
97    #[test]
98    fn parse_yesterday() {
99        assert!(matches!(
100            Period::from_str("yesterday"),
101            Ok(Period::Yesterday)
102        ));
103    }
104
105    #[test]
106    fn parse_week() {
107        assert!(matches!(Period::from_str("week"), Ok(Period::Week)));
108    }
109
110    #[test]
111    fn parse_hours() {
112        let period = Period::from_str("24h");
113        assert!(matches!(period, Ok(Period::Hours(24))));
114    }
115
116    #[test]
117    fn parse_days() {
118        let period = Period::from_str("7d");
119        assert!(matches!(period, Ok(Period::Days(7))));
120    }
121
122    #[test]
123    fn parse_custom_days() {
124        let period = Period::from_str("14d");
125        assert!(matches!(period, Ok(Period::Days(14))));
126    }
127
128    #[test]
129    fn parse_invalid_returns_error() {
130        assert!(Period::from_str("invalid").is_err());
131    }
132
133    #[test]
134    fn parse_invalid_number_returns_error() {
135        assert!(Period::from_str("abch").is_err());
136    }
137
138    #[test]
139    fn today_range_starts_at_midnight() {
140        let range = Period::Today.to_time_range();
141        assert_eq!(range.since.time().hour(), 0);
142        assert_eq!(range.since.time().minute(), 0);
143        assert!(range.until.is_none());
144    }
145
146    #[test]
147    fn yesterday_range_has_both_bounds() {
148        let range = Period::Yesterday.to_time_range();
149        assert!(range.until.is_some());
150        let until = range.until.as_ref().unwrap_or(&range.since);
151        assert!(range.since < *until);
152    }
153
154    #[test]
155    fn hours_range_is_in_past() {
156        let range = Period::Hours(24).to_time_range();
157        assert!(range.since < Local::now());
158        assert!(range.until.is_none());
159    }
160
161    #[test]
162    fn week_range_starts_on_monday() {
163        let range = Period::Week.to_time_range();
164        assert_eq!(range.since.weekday(), Weekday::Mon);
165    }
166}