1use std::fmt::Display;
15use std::str::FromStr;
16use std::{cmp::Ordering, sync::OnceLock};
17
18use crate::units::Angle;
19use num::ToPrimitive;
20use regex::Regex;
21use thiserror::Error;
22
23use super::subsecond::Subsecond;
24use crate::i64::consts::{
25 SECONDS_PER_DAY, SECONDS_PER_HALF_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
26};
27
28fn iso_regex() -> &'static Regex {
29 static ISO: OnceLock<Regex> = OnceLock::new();
30 ISO.get_or_init(|| {
31 Regex::new(r"(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<subsecond>\.\d+)?").unwrap()
32 })
33}
34
35#[derive(Debug, Copy, Clone, Error)]
38#[error("seconds must be in the range [0.0..86401.0) but was {0}")]
39pub struct InvalidSeconds(f64);
40
41impl PartialEq for InvalidSeconds {
42 fn eq(&self, other: &Self) -> bool {
43 self.0.total_cmp(&other.0) == Ordering::Equal
44 }
45}
46
47impl Eq for InvalidSeconds {}
48
49#[derive(Debug, Clone, Error, PartialEq, Eq)]
51pub enum TimeOfDayError {
52 #[error("hour must be in the range [0..24) but was {0}")]
54 InvalidHour(u8),
55 #[error("minute must be in the range [0..60) but was {0}")]
57 InvalidMinute(u8),
58 #[error("second must be in the range [0..61) but was {0}")]
60 InvalidSecond(u8),
61 #[error("second must be in the range [0..86401) but was {0}")]
63 InvalidSecondOfDay(u64),
64 #[error(transparent)]
66 InvalidSeconds(#[from] InvalidSeconds),
67 #[error("leap seconds are only valid at the end of the day")]
69 InvalidLeapSecond,
70 #[error("invalid ISO string `{0}`")]
72 InvalidIsoString(String),
73}
74
75pub trait CivilTime {
78 fn time(&self) -> TimeOfDay;
80
81 fn hour(&self) -> u8 {
83 self.time().hour()
84 }
85
86 fn minute(&self) -> u8 {
88 self.time().minute()
89 }
90
91 fn second(&self) -> u8 {
93 self.time().second()
94 }
95
96 fn as_seconds_f64(&self) -> f64 {
98 self.time().subsecond().as_seconds_f64() + self.time().second() as f64
99 }
100
101 fn millisecond(&self) -> u32 {
103 self.time().subsecond().milliseconds()
104 }
105
106 fn microsecond(&self) -> u32 {
108 self.time().subsecond().microseconds()
109 }
110
111 fn nanosecond(&self) -> u32 {
113 self.time().subsecond().nanoseconds()
114 }
115
116 fn picosecond(&self) -> u32 {
118 self.time().subsecond().picoseconds()
119 }
120
121 fn femtosecond(&self) -> u32 {
123 self.time().subsecond().femtoseconds()
124 }
125}
126
127#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct TimeOfDay {
131 hour: u8,
132 minute: u8,
133 second: u8,
134 subsecond: Subsecond,
135}
136
137impl TimeOfDay {
138 pub const MIDNIGHT: Self = TimeOfDay {
140 hour: 0,
141 minute: 0,
142 second: 0,
143 subsecond: Subsecond::ZERO,
144 };
145
146 pub const NOON: Self = TimeOfDay {
148 hour: 12,
149 minute: 0,
150 second: 0,
151 subsecond: Subsecond::ZERO,
152 };
153 pub fn new(hour: u8, minute: u8, second: u8) -> Result<Self, TimeOfDayError> {
161 if !(0..24).contains(&hour) {
162 return Err(TimeOfDayError::InvalidHour(hour));
163 }
164 if !(0..60).contains(&minute) {
165 return Err(TimeOfDayError::InvalidMinute(minute));
166 }
167 if !(0..61).contains(&second) {
168 return Err(TimeOfDayError::InvalidSecond(second));
169 }
170 Ok(Self {
171 hour,
172 minute,
173 second,
174 subsecond: Subsecond::default(),
175 })
176 }
177
178 pub fn from_iso(iso: &str) -> Result<Self, TimeOfDayError> {
188 let caps = iso_regex()
189 .captures(iso)
190 .ok_or(TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
191 let hour: u8 = caps["hour"]
192 .parse()
193 .map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
194 let minute: u8 = caps["minute"]
195 .parse()
196 .map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
197 let second: u8 = caps["second"]
198 .parse()
199 .map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
200 let mut time = TimeOfDay::new(hour, minute, second)?;
201 if let Some(subsecond) = caps.name("subsecond") {
202 let subsecond_str = subsecond.as_str().trim_start_matches('.');
203 let subsecond: Subsecond = subsecond_str
204 .parse()
205 .map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
206 time.with_subsecond(subsecond);
207 }
208 Ok(time)
209 }
210
211 pub fn from_hour(hour: u8) -> Result<Self, TimeOfDayError> {
213 Self::new(hour, 0, 0)
214 }
215
216 pub fn from_hour_and_minute(hour: u8, minute: u8) -> Result<Self, TimeOfDayError> {
218 Self::new(hour, minute, 0)
219 }
220
221 pub fn from_hms(hour: u8, minute: u8, seconds: f64) -> Result<Self, TimeOfDayError> {
230 if !(0.0..86401.0).contains(&seconds) {
231 return Err(TimeOfDayError::InvalidSeconds(InvalidSeconds(seconds)));
232 }
233 let second = seconds.trunc() as u8;
234 let fraction = seconds.fract();
235 let subsecond = Subsecond::from_f64(fraction)
236 .ok_or(TimeOfDayError::InvalidSeconds(InvalidSeconds(seconds)))?;
237 Ok(Self::new(hour, minute, second)?.with_subsecond(subsecond))
238 }
239
240 pub fn from_second_of_day(second_of_day: u64) -> Result<Self, TimeOfDayError> {
246 if !(0..86401).contains(&second_of_day) {
247 return Err(TimeOfDayError::InvalidSecondOfDay(second_of_day));
248 }
249 if second_of_day == SECONDS_PER_DAY as u64 {
250 return Self::new(23, 59, 60);
251 }
252 let hour = (second_of_day / 3600) as u8;
253 let minute = ((second_of_day % 3600) / 60) as u8;
254 let second = (second_of_day % 60) as u8;
255 Self::new(hour, minute, second)
256 }
257
258 pub fn from_seconds_since_j2000(seconds: i64) -> Self {
262 let mut second_of_day = (seconds + SECONDS_PER_HALF_DAY) % SECONDS_PER_DAY;
263 if second_of_day.is_negative() {
264 second_of_day += SECONDS_PER_DAY;
265 }
266 Self::from_second_of_day(
267 second_of_day
268 .to_u64()
269 .unwrap_or_else(|| unreachable!("second of day should be positive")),
270 )
271 .unwrap_or_else(|_| unreachable!("second of day should be in range"))
272 }
273
274 pub fn with_subsecond(&mut self, subsecond: Subsecond) -> Self {
276 self.subsecond = subsecond;
277 *self
278 }
279
280 pub fn hour(&self) -> u8 {
282 self.hour
283 }
284
285 pub fn minute(&self) -> u8 {
287 self.minute
288 }
289
290 pub fn second(&self) -> u8 {
292 self.second
293 }
294
295 pub fn subsecond(&self) -> Subsecond {
297 self.subsecond
298 }
299
300 pub fn seconds_f64(&self) -> f64 {
302 self.subsecond.as_seconds_f64() + self.second as f64
303 }
304
305 pub fn second_of_day(&self) -> i64 {
307 self.hour as i64 * SECONDS_PER_HOUR
308 + self.minute as i64 * SECONDS_PER_MINUTE
309 + self.second as i64
310 }
311
312 pub fn to_angle(&self) -> Angle {
314 Angle::from_hms(self.hour as i64, self.minute, self.seconds_f64())
315 }
316}
317
318impl Display for TimeOfDay {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 let precision = f.precision().unwrap_or(3);
321 write!(
322 f,
323 "{:02}:{:02}:{:02}{}",
324 self.hour,
325 self.minute,
326 self.second,
327 format!("{:.*}", precision, self.subsecond).trim_start_matches('0')
328 )
329 }
330}
331
332impl FromStr for TimeOfDay {
333 type Err = TimeOfDayError;
334
335 fn from_str(iso: &str) -> Result<Self, Self::Err> {
336 Self::from_iso(iso)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use rstest::rstest;
343
344 use super::*;
345
346 #[rstest]
347 #[case(43201, TimeOfDay::new(12, 0, 1))]
348 #[case(86399, TimeOfDay::new(23, 59, 59))]
349 #[case(86400, TimeOfDay::new(23, 59, 60))]
350 fn test_time_of_day_from_second_of_day(
351 #[case] second_of_day: u64,
352 #[case] expected: Result<TimeOfDay, TimeOfDayError>,
353 ) {
354 let actual = TimeOfDay::from_second_of_day(second_of_day);
355 assert_eq!(actual, expected);
356 }
357
358 #[test]
359 fn test_time_of_day_display() {
360 let subsecond: Subsecond = "123456789123456".parse().unwrap();
361 let time = TimeOfDay::new(12, 0, 0).unwrap().with_subsecond(subsecond);
362 assert_eq!(format!("{time}"), "12:00:00.123");
363 assert_eq!(format!("{time:.15}"), "12:00:00.123456789123456");
364 }
365
366 #[rstest]
367 #[case(TimeOfDay::new(24, 0, 0), Err(TimeOfDayError::InvalidHour(24)))]
368 #[case(TimeOfDay::new(0, 60, 0), Err(TimeOfDayError::InvalidMinute(60)))]
369 #[case(TimeOfDay::new(0, 0, 61), Err(TimeOfDayError::InvalidSecond(61)))]
370 #[case(
371 TimeOfDay::from_second_of_day(86401),
372 Err(TimeOfDayError::InvalidSecondOfDay(86401))
373 )]
374 #[case(TimeOfDay::from_hms(12, 0, -0.123), Err(TimeOfDayError::InvalidSeconds(InvalidSeconds(-0.123))))]
375 fn test_time_of_day_error(
376 #[case] actual: Result<TimeOfDay, TimeOfDayError>,
377 #[case] expected: Result<TimeOfDay, TimeOfDayError>,
378 ) {
379 assert_eq!(actual, expected);
380 }
381
382 #[rstest]
383 #[case("12:13:14", Ok(TimeOfDay::new(12, 13, 14).unwrap()))]
384 #[case("12:13:14.123", Ok(TimeOfDay::new(12, 13, 14).unwrap().with_subsecond("123".parse().unwrap())))]
385 #[case("2:13:14.123", Err(TimeOfDayError::InvalidIsoString("2:13:14.123".to_string())))]
386 #[case("12:3:14.123", Err(TimeOfDayError::InvalidIsoString("12:3:14.123".to_string())))]
387 #[case("12:13:4.123", Err(TimeOfDayError::InvalidIsoString("12:13:4.123".to_string())))]
388 fn test_time_of_day_from_string(
389 #[case] iso: &str,
390 #[case] expected: Result<TimeOfDay, TimeOfDayError>,
391 ) {
392 let actual: Result<TimeOfDay, TimeOfDayError> = iso.parse();
393 assert_eq!(actual, expected)
394 }
395
396 #[test]
397 fn test_invalid_seconds_eq() {
398 let a = InvalidSeconds(-f64::NAN);
399 let b = InvalidSeconds(f64::NAN);
400 assert_ne!(a, b);
402 let c = InvalidSeconds(f64::NAN);
404 let d = InvalidSeconds(f64::NAN);
405 assert_eq!(c, d);
406 }
407}