Skip to main content

guise/input/
time.rs

1//! `Time` — a wall-clock time of day plus parsing/formatting for the pickers.
2
3use std::fmt;
4
5/// Hour/minute time of day (24-hour internally).
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Time {
8  hour: u32,
9  minute: u32,
10}
11
12impl Time {
13  /// A validated time, or `None` when out of range.
14  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  /// Hour on a 12-hour clock (12, 1..=11) and whether it is PM.
31  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  /// The same time with a different hour/meridiem, e.g. from picker columns.
41  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  /// "14:05".
64  pub fn format_24(self) -> String {
65    format!("{:02}:{:02}", self.hour, self.minute)
66  }
67
68  /// "2:05 PM".
69  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  /// Parse "14:05", "2:05 PM", "2:05pm", "02:05 am".
80  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}