1use std::fmt::Debug;
29use std::fmt::Display;
30use std::str::FromStr;
31
32use strptime::ParseResult;
33use strptime::Parser;
34
35#[cfg(feature = "diesel-pg")]
36mod db;
37#[cfg(feature = "serde")]
38mod serde;
39
40#[derive(Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord)]
42#[cfg_attr(feature = "diesel-pg", derive(diesel::AsExpression, diesel::FromSqlRow))]
43#[cfg_attr(feature = "diesel-pg", diesel(sql_type = ::diesel::sql_types::Time))]
44pub struct WallClockTime {
45 seconds: u32,
47 micros: u32,
49}
50
51impl WallClockTime {
52 pub const fn new(hours: u8, minutes: u8, seconds: u8) -> Self {
59 Self::new_with_micros(hours, minutes, seconds, 0)
60 }
61
62 pub const fn new_with_micros(hours: u8, minutes: u8, seconds: u8, micros: u32) -> Self {
69 assert!(hours < 24, "Hours out of bounds.");
70 assert!(minutes < 60, "Minutes out of bounds.");
71 assert!(seconds < 60, "Seconds out of bounds.");
72 assert!(micros < 1_000_000, "Microseconds out of bounds.");
73 Self { seconds: hours as u32 * 3_600 + minutes as u32 * 60 + seconds as u32, micros }
74 }
75
76 pub const fn new_midnight_offset(seconds: u32, micros: u32) -> Self {
84 assert!(seconds < 86_400, "Seconds out of bounds.");
85 assert!(micros < 1_000_000, "Microseconds out of bounds.");
86 Self { seconds, micros }
87 }
88
89 pub fn parse(time_str: impl AsRef<str>, time_fmt: &'static str) -> ParseResult<Self> {
91 Ok(Parser::new(time_fmt).parse(time_str)?.time()?.into())
92 }
93
94 pub const fn hour(&self) -> u8 {
96 (self.seconds / 3_600) as u8
97 }
98
99 pub const fn minute(&self) -> u8 {
101 (self.seconds % 3600 / 60) as u8
102 }
103
104 pub const fn second(&self) -> u8 {
106 (self.seconds % 60) as u8
107 }
108
109 pub const fn microsecond(&self) -> u32 {
111 self.micros
112 }
113}
114
115impl Debug for WallClockTime {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 Display::fmt(self, f)
118 }
119}
120
121impl Display for WallClockTime {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "{:02}:{:02}:{:02}", self.hour(), self.minute(), self.second())?;
124 if self.micros > 0 {
125 write!(f, ".{:06}", self.micros)?;
126 }
127 Ok(())
128 }
129}
130
131impl FromStr for WallClockTime {
132 type Err = &'static str;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 let seconds_micros: Vec<&str> = s.split('.').collect();
136 if seconds_micros.len() > 2 {
137 Err("Only one `.` allowed in wall-clock times")?;
138 }
139 let micros = match seconds_micros.get(1) {
140 Some(micros) => micros.parse::<u32>().map_err(|_| "Invalid microseconds")?,
141 None => 0,
142 };
143 let hms = seconds_micros.first().ok_or("Empty string")?;
144 let hms: Vec<&str> = hms.split(':').collect();
145 if hms.len() != 3 {
146 Err("Invalid HH:MM:SS specified")?;
147 }
148 let hours = hms[0].parse::<u32>().map_err(|_| "Invalid HH")?;
149 let minutes = hms[1].parse::<u32>().map_err(|_| "Invalid MM")?;
150 let seconds = hms[2].parse::<u32>().map_err(|_| "Invalid SS")?;
151 Ok(Self { seconds: hours * 3600 + minutes * 60 + seconds, micros })
152 }
153}
154
155impl From<strptime::RawTime> for WallClockTime {
156 fn from(value: strptime::RawTime) -> Self {
157 Self::new_with_micros(
158 value.hour(),
159 value.minute(),
160 value.second(),
161 (value.nanosecond() / 1_000) as u32,
162 )
163 }
164}
165
166#[macro_export]
178macro_rules! time {
179 ($h:literal:$m:literal:$s:literal) => {{
180 #[allow(clippy::zero_prefixed_literal)]
181 {
182 $crate::WallClockTime::new($h, $m, $s)
183 }
184 }};
185}
186
187#[cfg(test)]
188mod tests {
189 use assert2::check;
190
191 use crate::WallClockTime;
192
193 #[test]
194 fn test_hours() {
195 check!(time!(09:30:00).hour() == 9);
196 check!(time!(16:00:00).hour() == 16);
197 check!(time!(17:15:30).hour() == 17);
198 }
199
200 #[test]
201 fn test_minutes() {
202 check!(time!(09:30:00).minute() == 30);
203 check!(time!(16:00:00).minute() == 0);
204 check!(time!(17:15:30).minute() == 15);
205 }
206
207 #[test]
208 fn test_seconds() {
209 check!(time!(16:00:00).second() == 0);
210 check!(time!(17:15:30).second() == 30);
211 }
212
213 #[test]
214 fn test_micros() {
215 check!(WallClockTime::new_with_micros(9, 30, 0, 0).microsecond() == 0);
216 check!(WallClockTime::new_with_micros(17, 15, 30, 600_000).microsecond() == 600_000);
217 }
218
219 #[test]
220 fn test_display() {
221 check!(time!(16:00:00).to_string() == "16:00:00");
222 check!(format!("{:?}", time!(16:00:00)) == "16:00:00");
223 check!(time!(17:15:30).to_string() == "17:15:30");
224 check!(WallClockTime::new_with_micros(17, 15, 30, 600_000).to_string() == "17:15:30.600000");
225 }
226
227 #[test]
228 fn test_parse() -> Result<(), &'static str> {
229 check!("09:30:00".parse::<WallClockTime>()? == time!(09:30:00));
230 check!("17:15:30".parse::<WallClockTime>()? == time!(17:15:30));
231 check!(
232 "18:30:01.345678".parse::<WallClockTime>()?
233 == WallClockTime::new_with_micros(18, 30, 1, 345_678)
234 );
235 Ok(())
236 }
237}