opening_hours_syntax/
extended_time.rs

1use std::convert::TryInto;
2use std::fmt::{Debug, Display};
3
4use chrono::{NaiveTime, Timelike};
5
6/// An hour+minute struct that can go up to 48h.
7#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct ExtendedTime {
9    hour: u8,
10    minute: u8,
11}
12
13impl ExtendedTime {
14    /// 00:00 (start of day)
15    pub const MIDNIGHT_00: Self = Self::new(0, 0).unwrap();
16
17    /// 24:00 (end of day)
18    pub const MIDNIGHT_24: Self = Self::new(24, 0).unwrap();
19
20    /// 48:00 (end of next day)
21    pub const MIDNIGHT_48: Self = Self::new(48, 0).unwrap();
22
23    /// Create a new extended time, this may return `None` if input values are
24    /// out of range.
25    ///
26    /// ```
27    /// use opening_hours_syntax::ExtendedTime;
28    ///
29    /// assert!(ExtendedTime::new(28, 30).is_some());
30    /// assert!(ExtendedTime::new(72, 15).is_none()); // hours are out of bound
31    /// assert!(ExtendedTime::new(24, 60).is_none()); // minutes are out of bound
32    /// ```
33    #[inline]
34    pub const fn new(hour: u8, minute: u8) -> Option<Self> {
35        if hour > 48 || minute > 59 || (hour == 48 && minute > 0) {
36            None
37        } else {
38            Some(Self { hour, minute })
39        }
40    }
41
42    /// Get the number of full hours in this extended time.
43    ///
44    /// ```
45    /// use opening_hours_syntax::ExtendedTime;
46    ///
47    /// let time = ExtendedTime::new(27, 35).unwrap();
48    /// assert_eq!(time.hour(), 27);
49    /// ```
50    #[inline]
51    pub fn hour(self) -> u8 {
52        self.hour
53    }
54
55    /// Get the number of remaining minutes in this extended time.
56    ///
57    /// ```
58    /// use opening_hours_syntax::ExtendedTime;
59    ///
60    /// let time = ExtendedTime::new(27, 35).unwrap();
61    /// assert_eq!(time.minute(), 35);
62    /// ```
63    #[inline]
64    pub fn minute(self) -> u8 {
65        self.minute
66    }
67
68    /// Add plain minutes to the extended time and return `None` if this
69    /// results in it being out of bounds.
70    ///
71    /// ```
72    /// use opening_hours_syntax::ExtendedTime;
73    ///
74    /// let time = ExtendedTime::new(24, 0).unwrap();
75    /// assert_eq!(time.add_minutes(75), ExtendedTime::new(25, 15));
76    /// assert!(time.add_minutes(24 * 60 + 1).is_none());
77    /// assert!(time.add_minutes(-24 * 60 - 1).is_none());
78    /// ```
79    #[inline]
80    pub fn add_minutes(self, minutes: i16) -> Option<Self> {
81        let as_minutes = (self.mins_from_midnight() as i16).checked_add(minutes)?;
82        Self::from_mins_from_midnight(as_minutes.try_into().ok()?)
83    }
84
85    /// Add plain hours to the extended time and return `None` if this results
86    /// in it being out of bounds.
87    ///
88    /// ```
89    /// use opening_hours_syntax::ExtendedTime;
90    ///
91    /// let time = ExtendedTime::new(24, 15).unwrap();
92    /// assert_eq!(time.add_hours(3), ExtendedTime::new(27, 15));
93    /// assert!(time.add_hours(25).is_none());
94    /// assert!(time.add_hours(-25).is_none());
95    /// ```
96    #[inline]
97    pub fn add_hours(self, hours: i8) -> Option<Self> {
98        Self::new(
99            (i16::from(self.hour) + i16::from(hours)).try_into().ok()?,
100            self.minute,
101        )
102    }
103
104    /// Get the total number of minutes from *00:00*.
105    ///
106    /// ```
107    /// use opening_hours_syntax::ExtendedTime;
108    ///
109    /// let time = ExtendedTime::new(25, 15).unwrap();
110    /// assert_eq!(time.mins_from_midnight(), 25 * 60 + 15);
111    /// ```
112    #[inline]
113    pub fn mins_from_midnight(self) -> u16 {
114        u16::from(self.minute) + 60 * u16::from(self.hour)
115    }
116
117    /// Build an extended time from the total number of minutes from midnight
118    /// and return `None` if the result is out of bounds.
119    ///
120    /// ```
121    /// use opening_hours_syntax::ExtendedTime;
122    ///
123    /// assert_eq!(
124    ///     ExtendedTime::from_mins_from_midnight(26 * 60 + 15),
125    ///     ExtendedTime::new(26, 15),
126    /// );
127    ///
128    /// assert!(ExtendedTime::from_mins_from_midnight(65_000).is_none());
129    /// ```
130    #[inline]
131    pub fn from_mins_from_midnight(minute: u16) -> Option<Self> {
132        let hour = (minute / 60).try_into().ok()?;
133        let minute = (minute % 60).try_into().ok()?;
134        Self::new(hour, minute)
135    }
136}
137
138impl Display for ExtendedTime {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        write!(f, "{:02}:{:02}", self.hour, self.minute)
141    }
142}
143
144impl Debug for ExtendedTime {
145    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
146        write!(f, "{self}")
147    }
148}
149
150impl TryInto<NaiveTime> for ExtendedTime {
151    type Error = ();
152
153    #[inline]
154    fn try_into(self) -> Result<NaiveTime, Self::Error> {
155        NaiveTime::from_hms_opt(self.hour.into(), self.minute.into(), 0).ok_or(())
156    }
157}
158
159impl From<NaiveTime> for ExtendedTime {
160    #[inline]
161    fn from(time: NaiveTime) -> ExtendedTime {
162        Self {
163            hour: time.hour().try_into().expect("invalid NaiveTime"),
164            minute: time.minute().try_into().expect("invalid NaiveTime"),
165        }
166    }
167}