1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Time {
8 hour: u32,
9 minute: u32,
10}
11
12impl Time {
13 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 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 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 pub fn format_24(self) -> String {
65 format!("{:02}:{:02}", self.hour, self.minute)
66 }
67
68 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 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}