work_tuimer/models/
time_point.rs1use 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}