Skip to main content

guise/input/
time.rs

1//! `Time` — a wall-clock time of day plus parsing/formatting for the pickers.
2
3use std::fmt;
4
5/// Hour/minute time of day (24-hour internally).
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Time {
8    hour: u32,
9    minute: u32,
10}
11
12impl Time {
13    /// A validated time, or `None` when out of range.
14    pub fn new(hour: u32, minute: u32) -> Option<Time> {
15        if hour < 24 && minute < 60 {
16            Some(Time { hour, minute })
17        } else {
18            None
19        }
20    }
21
22    pub fn hour(self) -> u32 {
23        self.hour
24    }
25
26    pub fn minute(self) -> u32 {
27        self.minute
28    }
29
30    /// Hour on a 12-hour clock (12, 1..=11) and whether it is PM.
31    pub fn hour_12(self) -> (u32, bool) {
32        let pm = self.hour >= 12;
33        let hour = match self.hour % 12 {
34            0 => 12,
35            h => h,
36        };
37        (hour, pm)
38    }
39
40    /// The same time with a different hour/meridiem, e.g. from picker columns.
41    pub fn with_hour_12(self, hour_12: u32, pm: bool) -> Time {
42        let base = hour_12 % 12;
43        Time {
44            hour: if pm { base + 12 } else { base },
45            minute: self.minute,
46        }
47    }
48
49    pub fn with_hour(self, hour: u32) -> Time {
50        Time {
51            hour: hour.min(23),
52            ..self
53        }
54    }
55
56    pub fn with_minute(self, minute: u32) -> Time {
57        Time {
58            minute: minute.min(59),
59            ..self
60        }
61    }
62
63    /// "14:05".
64    pub fn format_24(self) -> String {
65        format!("{:02}:{:02}", self.hour, self.minute)
66    }
67
68    /// "2:05 PM".
69    pub fn format_12(self) -> String {
70        let (hour, pm) = self.hour_12();
71        format!(
72            "{}:{:02} {}",
73            hour,
74            self.minute,
75            if pm { "PM" } else { "AM" }
76        )
77    }
78
79    /// Parse "14:05", "2:05 PM", "2:05pm", "02:05 am".
80    pub fn parse(s: &str) -> Option<Time> {
81        let s = s.trim();
82        let lower = s.to_ascii_lowercase();
83        let (clock, meridiem) = if let Some(rest) = lower.strip_suffix("pm") {
84            (rest.trim_end(), Some(true))
85        } else if let Some(rest) = lower.strip_suffix("am") {
86            (rest.trim_end(), Some(false))
87        } else {
88            (lower.as_str(), None)
89        };
90        let (h, m) = clock.split_once(':')?;
91        let hour: u32 = h.trim().parse().ok()?;
92        let minute: u32 = m.trim().parse().ok()?;
93        match meridiem {
94            None => Time::new(hour, minute),
95            Some(pm) => {
96                if hour == 0 || hour > 12 {
97                    return None;
98                }
99                let base = hour % 12;
100                Time::new(if pm { base + 12 } else { base }, minute)
101            }
102        }
103    }
104}
105
106impl fmt::Display for Time {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.write_str(&self.format_24())
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn validation() {
118        assert!(Time::new(23, 59).is_some());
119        assert!(Time::new(24, 0).is_none());
120        assert!(Time::new(0, 60).is_none());
121    }
122
123    #[test]
124    fn twelve_hour_conversion() {
125        assert_eq!(Time::new(0, 30).unwrap().hour_12(), (12, false));
126        assert_eq!(Time::new(12, 0).unwrap().hour_12(), (12, true));
127        assert_eq!(Time::new(13, 15).unwrap().hour_12(), (1, true));
128        assert_eq!(Time::new(11, 59).unwrap().hour_12(), (11, false));
129    }
130
131    #[test]
132    fn with_hour_12_round_trips() {
133        for hour in 0..24 {
134            let t = Time::new(hour, 42).unwrap();
135            let (h12, pm) = t.hour_12();
136            assert_eq!(t.with_hour_12(h12, pm), t);
137        }
138    }
139
140    #[test]
141    fn formatting() {
142        assert_eq!(Time::new(14, 5).unwrap().format_24(), "14:05");
143        assert_eq!(Time::new(14, 5).unwrap().format_12(), "2:05 PM");
144        assert_eq!(Time::new(0, 0).unwrap().format_12(), "12:00 AM");
145        assert_eq!(Time::new(12, 0).unwrap().format_12(), "12:00 PM");
146        assert_eq!(Time::new(9, 30).unwrap().to_string(), "09:30");
147    }
148
149    #[test]
150    fn parsing() {
151        assert_eq!(Time::parse("14:05"), Time::new(14, 5));
152        assert_eq!(Time::parse("2:05 PM"), Time::new(14, 5));
153        assert_eq!(Time::parse("2:05pm"), Time::new(14, 5));
154        assert_eq!(Time::parse("12:00 am"), Time::new(0, 0));
155        assert_eq!(Time::parse("12:00 PM"), Time::new(12, 0));
156        assert_eq!(Time::parse(" 09:30 "), Time::new(9, 30));
157        assert_eq!(Time::parse("25:00"), None);
158        assert_eq!(Time::parse("13:00 PM"), None);
159        assert_eq!(Time::parse("0:30 am"), None);
160        assert_eq!(Time::parse("nope"), None);
161    }
162}