initiative_core/time/
mod.rs

1pub use command::TimeCommand;
2pub use interval::Interval;
3
4mod command;
5mod interval;
6
7use std::fmt;
8use std::str::FromStr;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct Time {
12    days: i32,
13    hours: u8,
14    minutes: u8,
15    seconds: u8,
16}
17
18pub struct TimeShortView<'a>(&'a Time);
19
20pub struct TimeLongView<'a>(&'a Time);
21
22impl Time {
23    pub fn try_new(days: i32, hours: u8, minutes: u8, seconds: u8) -> Result<Self, ()> {
24        if hours < 24 && minutes < 60 && seconds < 60 {
25            Ok(Self {
26                days,
27                hours,
28                minutes,
29                seconds,
30            })
31        } else {
32            Err(())
33        }
34    }
35
36    pub fn checked_add(&self, interval: &Interval) -> Option<Self> {
37        let (mut days, mut hours, mut minutes, mut seconds) = (
38            (self.days as i64) + (interval.days as i64),
39            (self.hours as i64) + (interval.hours as i64),
40            (self.minutes as i64) + (interval.minutes as i64),
41            (self.seconds as i64) + (interval.seconds as i64),
42        );
43
44        if interval.rounds != 0 {
45            seconds += interval.rounds as i64 * 6;
46        }
47
48        if !(0..60).contains(&seconds) {
49            minutes += seconds.div_euclid(60);
50            seconds = seconds.rem_euclid(60);
51        }
52
53        if !(0..60).contains(&minutes) {
54            hours += minutes.div_euclid(60);
55            minutes = minutes.rem_euclid(60);
56        }
57
58        if !(0..24).contains(&hours) {
59            days += hours.div_euclid(24);
60            hours = hours.rem_euclid(24);
61        }
62
63        if let (Ok(days), Ok(hours), Ok(minutes), Ok(seconds)) = (
64            days.try_into(),
65            hours.try_into(),
66            minutes.try_into(),
67            seconds.try_into(),
68        ) {
69            Some(Self {
70                days,
71                hours,
72                minutes,
73                seconds,
74            })
75        } else {
76            None
77        }
78    }
79
80    pub fn checked_sub(&self, interval: &Interval) -> Option<Self> {
81        if let (Some(days), Some(hours), Some(minutes), Some(seconds), Some(rounds)) = (
82            0i32.checked_sub(interval.days),
83            0i32.checked_sub(interval.hours),
84            0i32.checked_sub(interval.minutes),
85            0i32.checked_sub(interval.seconds),
86            0i32.checked_sub(interval.rounds),
87        ) {
88            self.checked_add(&Interval {
89                days,
90                hours,
91                minutes,
92                seconds,
93                rounds,
94            })
95        } else {
96            None
97        }
98    }
99
100    pub fn display_short(&self) -> TimeShortView {
101        TimeShortView(self)
102    }
103
104    pub fn display_long(&self) -> TimeLongView {
105        TimeLongView(self)
106    }
107}
108
109impl Default for Time {
110    fn default() -> Self {
111        Self {
112            days: 1,
113            hours: 8,
114            minutes: 0,
115            seconds: 0,
116        }
117    }
118}
119
120impl FromStr for Time {
121    type Err = ();
122
123    fn from_str(raw: &str) -> Result<Self, Self::Err> {
124        let mut parts = raw.split(':');
125
126        let days = parts.next().ok_or(())?.parse().map_err(|_| ())?;
127        let hours = parts.next().ok_or(())?.parse().map_err(|_| ())?;
128        let minutes = parts.next().ok_or(())?.parse().map_err(|_| ())?;
129        let seconds = parts.next().ok_or(())?.parse().map_err(|_| ())?;
130
131        if parts.next().is_none() {
132            Time::try_new(days, hours, minutes, seconds)
133        } else {
134            Err(())
135        }
136    }
137}
138
139impl fmt::Display for TimeShortView<'_> {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        let time = self.0;
142        write!(
143            f,
144            "{}:{:02}:{:02}:{:02}",
145            time.days, time.hours, time.minutes, time.seconds
146        )
147    }
148}
149
150impl fmt::Display for TimeLongView<'_> {
151    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152        let time = self.0;
153
154        let (hours, am_pm) = match time.hours {
155            0 => (12, "am"),
156            1..=11 => (time.hours, "am"),
157            12 => (12, "pm"),
158            _ => (time.hours % 12, "pm"),
159        };
160
161        write!(
162            f,
163            "day {} at {}:{:02}:{:02} {}",
164            time.days, hours, time.minutes, time.seconds, am_pm
165        )
166    }
167}
168
169#[cfg(test)]
170mod test {
171    use super::*;
172
173    #[test]
174    fn time_try_new_test() {
175        assert_eq!(Ok(t(1, 2, 3, 4)), Time::try_new(1, 2, 3, 4));
176        assert!(Time::try_new(i32::MAX, 23, 59, 59).is_ok());
177        assert_eq!(Err(()), Time::try_new(0, 24, 0, 0));
178        assert_eq!(Err(()), Time::try_new(0, 0, 60, 0));
179        assert_eq!(Err(()), Time::try_new(0, 0, 0, 60));
180    }
181
182    #[test]
183    fn time_default_test() {
184        assert_eq!(t(1, 8, 0, 0), Time::default());
185    }
186
187    #[test]
188    fn time_checked_add_test() {
189        assert_eq!(
190            t(3, 6, 9, 12),
191            t(1, 2, 3, 4)
192                .checked_add(&Interval::new(2, 4, 6, 8, 0))
193                .unwrap(),
194        );
195
196        assert_eq!(
197            t(1, 1, 1, 1),
198            t0().checked_add(&Interval::new_seconds(86400 + 3600 + 60 + 1))
199                .unwrap(),
200        );
201
202        assert_eq!(
203            t(1, 1, 1, 0),
204            t0().checked_add(&Interval::new_minutes(1440 + 60 + 1))
205                .unwrap(),
206        );
207
208        assert_eq!(
209            t(1, 1, 0, 0),
210            t0().checked_add(&Interval::new_hours(24 + 1)).unwrap(),
211        );
212
213        assert_eq!(
214            t(2, 0, 0, 0),
215            t(1, 23, 59, 59)
216                .checked_add(&Interval::new_seconds(1))
217                .unwrap(),
218        );
219
220        assert_eq!(
221            t(0, 0, 0, 6),
222            t0().checked_add(&Interval::new_rounds(1)).unwrap(),
223        );
224        assert_eq!(
225            t(0, 0, 1, 0),
226            t0().checked_add(&Interval::new_rounds(10)).unwrap(),
227        );
228    }
229
230    #[test]
231    fn time_checked_add_test_limits() {
232        assert!(tmax().checked_add(&Interval::new_days(1)).is_none());
233        assert!(t1()
234            .checked_add(&Interval::new_days(i32::MAX - 1))
235            .is_some());
236        assert!(t1().checked_add(&Interval::new_days(i32::MAX)).is_none());
237
238        assert!(tmax().checked_add(&Interval::new_hours(1)).is_none());
239        assert!(t1().checked_add(&Interval::new_hours(i32::MAX)).is_some());
240
241        assert!(tmax().checked_add(&Interval::new_minutes(1)).is_none());
242        assert!(t1().checked_add(&Interval::new_minutes(i32::MAX)).is_some());
243
244        assert!(tmax().checked_add(&Interval::new_seconds(1)).is_none());
245        assert!(t1().checked_add(&Interval::new_seconds(i32::MAX)).is_some());
246
247        assert!(tmax().checked_add(&Interval::new_rounds(1)).is_none());
248        assert!(t1().checked_add(&Interval::new_rounds(i32::MAX)).is_some());
249    }
250
251    #[test]
252    fn time_checked_sub_test() {
253        assert_eq!(
254            t(-1, 0, 0, 0),
255            t0().checked_sub(&Interval::new_days(1)).unwrap(),
256        );
257
258        assert_eq!(
259            t0(),
260            t(0, 1, 0, 0).checked_sub(&Interval::new_hours(1)).unwrap(),
261        );
262        assert_eq!(
263            t(-1, 23, 0, 0),
264            t0().checked_sub(&Interval::new_hours(1)).unwrap(),
265        );
266
267        assert_eq!(
268            t0(),
269            t(0, 0, 1, 0)
270                .checked_sub(&Interval::new_minutes(1))
271                .unwrap(),
272        );
273        assert_eq!(
274            t(-1, 23, 59, 0),
275            t0().checked_sub(&Interval::new_minutes(1)).unwrap(),
276        );
277
278        assert_eq!(
279            t0(),
280            t(0, 0, 0, 1)
281                .checked_sub(&Interval::new_seconds(1))
282                .unwrap(),
283        );
284        assert_eq!(
285            t(-1, 23, 59, 59),
286            t0().checked_sub(&Interval::new_seconds(1)).unwrap(),
287        );
288    }
289
290    #[test]
291    fn time_checked_sub_test_limits() {
292        assert!(t0().checked_sub(&Interval::new_days(i32::MAX)).is_some());
293        assert!(t0().checked_sub(&Interval::new_days(i32::MIN)).is_none());
294
295        assert!(t0().checked_sub(&Interval::new_hours(i32::MAX)).is_some());
296        assert!(t0().checked_sub(&Interval::new_hours(i32::MIN)).is_none());
297
298        assert!(t0().checked_sub(&Interval::new_minutes(i32::MAX)).is_some());
299        assert!(t0().checked_sub(&Interval::new_minutes(i32::MIN)).is_none());
300
301        assert!(t0().checked_sub(&Interval::new_seconds(i32::MAX)).is_some());
302        assert!(t0().checked_sub(&Interval::new_seconds(i32::MIN)).is_none());
303    }
304
305    #[test]
306    fn time_display_short_test() {
307        assert_eq!("1:02:03:04", t(1, 2, 3, 4).display_short().to_string());
308        assert_eq!("1:23:59:59", t(1, 23, 59, 59).display_short().to_string());
309    }
310
311    #[test]
312    fn time_display_long_test() {
313        assert_eq!("day 0 at 12:00:00 am", t0().display_long().to_string());
314        assert_eq!(
315            "day 1 at 1:02:03 am",
316            t(1, 1, 2, 3).display_long().to_string(),
317        );
318        assert_eq!(
319            "day 2 at 11:59:59 am",
320            t(2, 11, 59, 59).display_long().to_string(),
321        );
322        assert_eq!(
323            "day 3 at 12:00:00 pm",
324            t(3, 12, 0, 0).display_long().to_string(),
325        );
326        assert_eq!(
327            "day 4 at 1:00:00 pm",
328            t(4, 13, 0, 0).display_long().to_string(),
329        );
330        assert_eq!(
331            "day 5 at 11:59:59 pm",
332            t(5, 23, 59, 59).display_long().to_string(),
333        );
334    }
335
336    #[test]
337    fn time_from_str_test() {
338        assert_eq!(Ok(t(1, 2, 3, 4)), "1:02:03:04".parse());
339        assert_eq!(Ok(t(1, 23, 59, 59)), "1:23:59:59".parse());
340    }
341
342    fn t(days: i32, hours: u8, minutes: u8, seconds: u8) -> Time {
343        Time {
344            days,
345            hours,
346            minutes,
347            seconds,
348        }
349    }
350
351    fn t0() -> Time {
352        t(0, 0, 0, 0)
353    }
354
355    fn t1() -> Time {
356        t(1, 1, 1, 1)
357    }
358
359    fn tmax() -> Time {
360        t(i32::MAX, 23, 59, 59)
361    }
362}