work_tuimer/models/
time_point.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
6pub struct TimePoint {
7    pub hour: u8,
8    pub minute: u8,
9}
10
11impl TimePoint {
12    pub fn new(hour: u8, minute: u8) -> Result<Self, String> {
13        if hour >= 24 {
14            return Err(format!("Hour must be 0-23, got {}", hour));
15        }
16        if minute >= 60 {
17            return Err(format!("Minute must be 0-59, got {}", minute));
18        }
19        Ok(TimePoint { hour, minute })
20    }
21
22    pub fn from_minutes_since_midnight(minutes: u32) -> Result<Self, String> {
23        if minutes >= 24 * 60 {
24            return Err(format!("Minutes must be < 1440, got {}", minutes));
25        }
26        Ok(TimePoint {
27            hour: (minutes / 60) as u8,
28            minute: (minutes % 60) as u8,
29        })
30    }
31
32    pub fn to_minutes_since_midnight(self) -> u32 {
33        (self.hour as u32) * 60 + (self.minute as u32)
34    }
35
36    pub fn parse(s: &str) -> Result<Self, String> {
37        let parts: Vec<&str> = s.split(':').collect();
38        if parts.len() != 2 {
39            return Err(format!("Invalid time format: {}", s));
40        }
41
42        let hour = parts[0]
43            .parse::<u8>()
44            .map_err(|_| format!("Invalid hour: {}", parts[0]))?;
45        let minute = parts[1]
46            .parse::<u8>()
47            .map_err(|_| format!("Invalid minute: {}", parts[1]))?;
48
49        Self::new(hour, minute)
50    }
51}
52
53impl fmt::Display for TimePoint {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{:02}:{:02}", self.hour, self.minute)
56    }
57}
58
59impl FromStr for TimePoint {
60    type Err = String;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        TimePoint::parse(s)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_new_valid_time() {
73        let time = TimePoint::new(14, 30).unwrap();
74        assert_eq!(time.hour, 14);
75        assert_eq!(time.minute, 30);
76    }
77
78    #[test]
79    fn test_new_boundary_values() {
80        assert!(TimePoint::new(0, 0).is_ok());
81        assert!(TimePoint::new(23, 59).is_ok());
82    }
83
84    #[test]
85    fn test_new_invalid_hour() {
86        assert!(TimePoint::new(24, 0).is_err());
87        assert!(TimePoint::new(25, 30).is_err());
88    }
89
90    #[test]
91    fn test_new_invalid_minute() {
92        assert!(TimePoint::new(12, 60).is_err());
93        assert!(TimePoint::new(12, 99).is_err());
94    }
95
96    #[test]
97    fn test_parse_valid_time() {
98        let time = TimePoint::parse("14:30").unwrap();
99        assert_eq!(time.hour, 14);
100        assert_eq!(time.minute, 30);
101    }
102
103    #[test]
104    fn test_parse_with_leading_zeros() {
105        let time = TimePoint::parse("09:05").unwrap();
106        assert_eq!(time.hour, 9);
107        assert_eq!(time.minute, 5);
108    }
109
110    #[test]
111    fn test_parse_without_leading_zeros() {
112        let time = TimePoint::parse("9:5").unwrap();
113        assert_eq!(time.hour, 9);
114        assert_eq!(time.minute, 5);
115    }
116
117    #[test]
118    fn test_parse_invalid_format() {
119        assert!(TimePoint::parse("14").is_err());
120        assert!(TimePoint::parse("14:30:00").is_err());
121        assert!(TimePoint::parse("not a time").is_err());
122        assert!(TimePoint::parse("").is_err());
123    }
124
125    #[test]
126    fn test_parse_invalid_values() {
127        assert!(TimePoint::parse("24:00").is_err());
128        assert!(TimePoint::parse("12:60").is_err());
129        assert!(TimePoint::parse("-1:30").is_err());
130    }
131
132    #[test]
133    fn test_to_minutes_since_midnight() {
134        assert_eq!(TimePoint::new(0, 0).unwrap().to_minutes_since_midnight(), 0);
135        assert_eq!(
136            TimePoint::new(1, 0).unwrap().to_minutes_since_midnight(),
137            60
138        );
139        assert_eq!(
140            TimePoint::new(14, 30).unwrap().to_minutes_since_midnight(),
141            870
142        );
143        assert_eq!(
144            TimePoint::new(23, 59).unwrap().to_minutes_since_midnight(),
145            1439
146        );
147    }
148
149    #[test]
150    fn test_from_minutes_since_midnight() {
151        let time = TimePoint::from_minutes_since_midnight(0).unwrap();
152        assert_eq!(time, TimePoint::new(0, 0).unwrap());
153
154        let time = TimePoint::from_minutes_since_midnight(60).unwrap();
155        assert_eq!(time, TimePoint::new(1, 0).unwrap());
156
157        let time = TimePoint::from_minutes_since_midnight(870).unwrap();
158        assert_eq!(time, TimePoint::new(14, 30).unwrap());
159
160        let time = TimePoint::from_minutes_since_midnight(1439).unwrap();
161        assert_eq!(time, TimePoint::new(23, 59).unwrap());
162    }
163
164    #[test]
165    fn test_from_minutes_invalid() {
166        assert!(TimePoint::from_minutes_since_midnight(1440).is_err());
167        assert!(TimePoint::from_minutes_since_midnight(9999).is_err());
168    }
169
170    #[test]
171    fn test_roundtrip_conversion() {
172        let original = TimePoint::new(14, 30).unwrap();
173        let minutes = original.to_minutes_since_midnight();
174        let converted = TimePoint::from_minutes_since_midnight(minutes).unwrap();
175        assert_eq!(original, converted);
176    }
177
178    #[test]
179    fn test_display_format() {
180        assert_eq!(TimePoint::new(9, 5).unwrap().to_string(), "09:05");
181        assert_eq!(TimePoint::new(14, 30).unwrap().to_string(), "14:30");
182        assert_eq!(TimePoint::new(0, 0).unwrap().to_string(), "00:00");
183        assert_eq!(TimePoint::new(23, 59).unwrap().to_string(), "23:59");
184    }
185
186    #[test]
187    fn test_from_str_trait() {
188        let time: TimePoint = "14:30".parse().unwrap();
189        assert_eq!(time.hour, 14);
190        assert_eq!(time.minute, 30);
191    }
192
193    #[test]
194    fn test_ordering() {
195        let time1 = TimePoint::new(9, 0).unwrap();
196        let time2 = TimePoint::new(14, 30).unwrap();
197        let time3 = TimePoint::new(14, 30).unwrap();
198
199        assert!(time1 < time2);
200        assert!(time2 > time1);
201        assert_eq!(time2, time3);
202    }
203
204    #[test]
205    fn test_clone_and_copy() {
206        let time1 = TimePoint::new(14, 30).unwrap();
207        let time2 = time1;
208        assert_eq!(time1, time2);
209    }
210}