wall_clock/
lib.rs

1//! `wall-clock` provides a simple and very basic struct for repsenting time as one reads it off a
2//! clock on the wall, e.g. with no concept of date, or time zone.
3//!
4//! ## Examples
5//!
6//! Making a wall clock time:
7//!
8//! ```rs
9//! use wall_clock::WallClockTime;
10//!
11//! let wct = WallClockTime::new(15, 0, 0);
12//! ```
13//!
14//! You can also use the `time!` macro to get a syntax resembling a literal:
15//!
16//! ```rs
17//! use wall_clock::time;
18//! let wct = time!(15:00:00);
19//! ```
20//!
21//! ## Features
22//!
23//! `wall-clock` ships with the following features:
24//!
25//! - **diesel-pg**: Enables interop with PostgreSQL `TIME` columns using Diesel.
26//! - **serde**: Enables serialization and deserialization with `serde`. _(Enabled by default.)_
27
28use 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/// A representation of a time, as read from a wall clock, independent of date or time zone.
41#[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  /// The number of seconds elapsed since midnight.
46  seconds: u32,
47  /// The number of microseconds elapsed since `seconds`.
48  micros: u32,
49}
50
51impl WallClockTime {
52  /// A new wall-clock time set to the provided hours, minutes, and seconds.
53  ///
54  /// ## Panic
55  ///
56  /// Panics if any values are too high for a wall clock (hours >= 24, minutes >= 60, seconds >=
57  /// 60). Wall clocks don't know about leap seconds.
58  pub const fn new(hours: u8, minutes: u8, seconds: u8) -> Self {
59    Self::new_with_micros(hours, minutes, seconds, 0)
60  }
61
62  /// A new wall-clock time set to the provided hours, minutes, seconds, and microseconds.
63  ///
64  /// ## Panic
65  ///
66  /// Panics if any values are too high for a wall clock (hours >= 24, minutes >= 60, seconds >=
67  /// 60). Wall clocks don't know about leap seconds.
68  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  /// A new wall-clock time corresponding to the number of seconds and microseconds offset from
77  /// midnight.
78  ///
79  /// ## Panic
80  ///
81  /// Panics if any values are higher than is valid for a wall clock (seconds >= 86,400; micros >=
82  /// 1,000,000).
83  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  /// Parse a wall clock time from a string. Any date components are ignored.
90  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  /// The number of hours since midnight.
95  pub const fn hour(&self) -> u8 {
96    (self.seconds / 3_600) as u8
97  }
98
99  /// The number of minutes since the last hour.
100  pub const fn minute(&self) -> u8 {
101    (self.seconds % 3600 / 60) as u8
102  }
103
104  /// The number of seconds since the last minute.
105  pub const fn second(&self) -> u8 {
106    (self.seconds % 60) as u8
107  }
108
109  /// The number of microseconds since the last second.
110  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/// Construct a wall clock time from a `HH:MM:SS` literal.
167///
168/// ## Examples
169///
170/// ```
171/// # use wall_clock::time;
172/// let t = time!(15:30:45);
173/// assert_eq!(t.hour(), 15);
174/// assert_eq!(t.minute(), 30);
175/// assert_eq!(t.second(), 45);
176/// ```
177#[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}