office_hours/
lib.rs

1//! Give your code a break like any good employer!
2//!
3//! Are you tired of your code working all day every day? Don't you feel bad that your code keeps working while you're off
4//! relaxing and having fun after work?
5//!
6//! Well now you can use the power of **office-hours** to only run your code within the working hours of the day!
7//!
8//! # Important
9//!
10//! At the time of writing, the office hours are determined from the **Local Time Zone**
11//! of the host machine where the code is running. I might consider updating this library
12//! to support other timezones if I **really** want to suffer :P
13//!
14//! # Quick Start
15//!
16//! ```
17//! use chrono::Timelike;
18//! use office_hours::{Clock, OfficeHours};
19//!
20//! // 9am to 5pm are the default office hours
21//! let office_hours = OfficeHours::default();
22//! if office_hours.now() {
23//!     println!("Blimey! Is it time for work already?");
24//! } else {
25//!     println!("Phew, still on break!");
26//! }
27//!
28//! // 12pm to 5pm
29//! let afternoon = OfficeHours::new(Clock::TwelvePm, Clock::FivePm);
30//! if afternoon.now() {
31//!     println!("Blimey! Is it time for work already?");
32//! } else {
33//!     println!("Phew, still on break!");
34//! }
35//!
36//! // We can also cross the day boundary - 10pm to 6am
37//! let night_shift = OfficeHours::new(Clock::TenPm, Clock::SixAm);
38//! if night_shift.now() {
39//!     println!("Blimey! Is it time for work already?");
40//! } else {
41//!     println!("Phew, still on break!");
42//! }
43//!
44//! // Iterate over office hours as chrono::NaiveTime
45//! for time in office_hours.iter() {
46//!     println!("Hour: {:?}", time.hour());
47//!     println!("Minute: {:?}", time.minute());
48//!     println!("Second: {:?}", time.second());
49//! }
50//!
51//! // Iterate over office hours as u32
52//! for hour in office_hours.hours_iter() {
53//!     println!("Hour: {:?}", hour);
54//! }
55//! ```
56#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
57use std::cmp::Ordering;
58
59use chrono::{Duration, Local, NaiveTime, Timelike};
60
61use thiserror::Error;
62use OfficeHoursError::InvalidTimeSlice;
63
64mod r#macro;
65
66#[derive(Error, Debug)]
67pub enum OfficeHoursError<'a> {
68    #[error("Could not convert `{0:?}` to chrono::NaiveTime")]
69    InvalidTimeSlice(&'a [u32]),
70}
71
72/// Simple enum to store all the valid hours in a day.
73pub enum Clock {
74    TwelveAm = 0,
75    OneAm = 1,
76    TwoAm = 2,
77    ThreeAm = 3,
78    FourAm = 4,
79    FiveAm = 5,
80    SixAm = 6,
81    SevenAm = 7,
82    EightAm = 8,
83    NineAm = 9,
84    TenAm = 10,
85    ElevenAm = 11,
86    TwelvePm = 12,
87    OnePm = 13,
88    TwoPm = 14,
89    ThreePm = 15,
90    FourPm = 16,
91    FivePm = 17,
92    SixPm = 18,
93    SevenPm = 19,
94    EightPm = 20,
95    NinePm = 21,
96    TenPm = 22,
97    ElevenPm = 23,
98}
99
100/// Trait to implement helper functions in the style of [`From`].
101pub trait FromNaiveTime {
102    /// Makes a new [`NaiveTime`] out of a `&[u32]` slice.
103    ///
104    /// The slice supports a maximum length of 4 parts: `hour`,
105    /// `minute`, `second`, and `millisecond`.
106    ///
107    /// If any part is missing, it is substituted with a `0`.
108    ///
109    /// If an empty slice is given, it will default
110    /// to midnight.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`Err`] if an invalid hour, minute, second,
115    /// and/or millisecond.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use chrono::NaiveTime;
121    /// use office_hours::FromNaiveTime;
122    ///
123    /// let midnight = NaiveTime::from_slice_u32(&[]).unwrap();
124    /// assert_eq!(midnight, NaiveTime::default());
125    ///
126    /// let nine = NaiveTime::from_slice_u32(&[9]).unwrap();
127    /// assert_eq!(nine, NaiveTime::from_hms_milli_opt(9, 0, 0, 0).unwrap());
128    ///
129    /// let nine_thirty = NaiveTime::from_slice_u32(&[9, 30]).unwrap();
130    /// assert_eq!(
131    ///     nine_thirty,
132    ///     NaiveTime::from_hms_milli_opt(9, 30, 0, 0).unwrap()
133    /// );
134    /// ```
135    ///
136    /// See [`NaiveTime::from_hms_milli_opt`] for further information.
137    fn from_slice_u32(slice: &[u32]) -> Result<NaiveTime, OfficeHoursError>;
138
139    /// Makes a new [`NaiveTime`] out of a [`Clock`].
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use chrono::NaiveTime;
145    /// use office_hours::{Clock, FromNaiveTime};
146    /// let twelve_pm = NaiveTime::from_time(Clock::TwelvePm);
147    /// assert_eq!(
148    ///     twelve_pm,
149    ///     NaiveTime::from_hms_milli_opt(12, 0, 0, 0).unwrap()
150    /// )
151    /// ```
152    fn from_time(time: Clock) -> NaiveTime;
153
154    /// Makes a new [`NaiveTime`] from the hour given as a [`u32`].
155    ///
156    /// # Errors
157    ///
158    /// Returns [`None`] when given an invalid hour.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use chrono::NaiveTime;
164    /// use office_hours::FromNaiveTime;
165    /// let twelve_pm = NaiveTime::from_time_u32(12).unwrap();
166    /// assert_eq!(
167    ///     twelve_pm,
168    ///     NaiveTime::from_hms_milli_opt(12, 0, 0, 0).unwrap()
169    /// )
170    /// ```
171    fn from_time_u32(hour: u32) -> Option<NaiveTime>;
172}
173
174impl FromNaiveTime for NaiveTime {
175    fn from_slice_u32(slice: &[u32]) -> Result<NaiveTime, OfficeHoursError> {
176        let time = Self::from_hms_milli_opt(
177            *slice.first().unwrap_or(&0),
178            *slice.get(1).unwrap_or(&0),
179            *slice.get(2).unwrap_or(&0),
180            *slice.get(3).unwrap_or(&0),
181        )
182        .ok_or(InvalidTimeSlice(slice))?;
183        Ok(time)
184    }
185
186    fn from_time(hour: Clock) -> NaiveTime {
187        // -- SAFETY --
188        // Make sure all variants of the [Clock] enum continues
189        // to hold hours of the day that valid (0 -> 23)
190        unsafe { Self::from_time_u32(hour as u32).unwrap_unchecked() }
191    }
192
193    fn from_time_u32(hour: u32) -> Option<NaiveTime> {
194        Self::from_hms_opt(hour, 0, 0)
195    }
196}
197
198/// Simple struct to store the start and the finish
199/// of the office day.
200#[derive(Debug, Eq, PartialEq)]
201pub struct OfficeHours {
202    pub start: NaiveTime,
203    pub finish: NaiveTime,
204}
205
206impl Default for OfficeHours {
207    /// The default office hours are 9am to 5pm.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use chrono::NaiveTime;
213    /// use office_hours::{Clock, FromNaiveTime, OfficeHours};
214    ///
215    /// let office_hours = OfficeHours::default();
216    /// assert_eq!(office_hours.start, NaiveTime::from_time(Clock::NineAm));
217    /// assert_eq!(office_hours.finish, NaiveTime::from_time(Clock::FivePm));
218    /// ```
219    fn default() -> Self {
220        Self {
221            start: NaiveTime::from_time(Clock::NineAm),
222            finish: NaiveTime::from_time(Clock::FivePm),
223        }
224    }
225}
226
227impl OfficeHours {
228    /// Makes a new [`OfficeHours`] from the starting and finishing time.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use office_hours::{Clock, OfficeHours};
234    /// let morning = OfficeHours::new(Clock::NineAm, Clock::TwelvePm);
235    /// let afternoon = OfficeHours::new(Clock::TwelvePm, Clock::FivePm);
236    /// ```
237    #[must_use]
238    pub fn new(start: Clock, finish: Clock) -> Self {
239        Self {
240            start: NaiveTime::from_time(start),
241            finish: NaiveTime::from_time(finish),
242        }
243    }
244
245    /// Returns an iterator over the office hours.
246    ///
247    /// The iterator yields all [`NaiveTime`] instances from
248    /// the starting hour to finishing hour.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use office_hours::{Clock, OfficeHours};
254    ///
255    /// let office_hours = OfficeHours::default();
256    /// for time in office_hours.iter() {
257    ///     println!("Hours: {:?}", time);
258    /// }
259    /// ```
260    #[must_use]
261    pub const fn iter(&self) -> OfficeHoursIter {
262        OfficeHoursIter {
263            start: self.start,
264            finish: self.finish,
265        }
266    }
267
268    /// Collect the contents of [`Self::hours_iter`] into a [`Vec<u32>`].
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use office_hours::{Clock, OfficeHours};
274    ///
275    /// let office_hours = OfficeHours::default();
276    /// assert_eq!(office_hours.hours(), vec![9, 10, 11, 12, 13, 14, 15, 16]);
277    /// let night_shift = OfficeHours::new(Clock::TenPm, Clock::FiveAm);
278    /// assert_eq!(night_shift.hours(), vec![22, 23, 0, 1, 2, 3, 4]);
279    /// ```
280    #[must_use]
281    pub fn hours(&self) -> Vec<u32> {
282        self.hours_iter().collect()
283    }
284
285    /// Returns an iterator over the office hours.
286    ///
287    /// The iterator yields all hours from
288    /// the starting hour to finishing hour in its
289    /// raw [`u32`] form.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use office_hours::{Clock, OfficeHours};
295    ///
296    /// let office_hours = OfficeHours::default();
297    /// for hour in office_hours.hours_iter() {
298    ///     println!("u32: {:?}", hour);
299    /// }
300    /// ```
301    pub fn hours_iter(&self) -> impl Iterator<Item = u32> {
302        self.iter().map(|time| time.hour())
303    }
304
305    /// Checks if the current local time is within the office hours.
306    ///
307    /// # Examples
308    ///
309    /// ```
310    /// use office_hours::OfficeHours;
311    ///
312    /// let office_hours = OfficeHours::default();
313    /// if office_hours.now() {
314    ///     println!("Blimey! Is it time for work already?")
315    /// } else {
316    ///     println!("Phew, still on break!")
317    /// }
318    /// ```
319    #[must_use]
320    pub fn now(&self) -> bool {
321        Self::now_from_time(&self.start, &self.finish, &Local::now().time())
322    }
323
324    #[doc(hidden)]
325    #[must_use]
326    pub fn now_from_time(start: &NaiveTime, finish: &NaiveTime, now: &NaiveTime) -> bool {
327        match start.cmp(finish) {
328            Ordering::Equal => start == now,
329            Ordering::Less => (start..finish).contains(&now),
330            Ordering::Greater => now >= start || now < finish,
331        }
332    }
333}
334
335impl<'a> TryFrom<(&'a [u32], &'a [u32])> for OfficeHours {
336    type Error = OfficeHoursError<'a>;
337
338    /// Performs the conversion.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use office_hours::OfficeHours;
344    ///
345    /// let start: &[u32] = &[9, 30];
346    /// let finish: &[u32] = &[17, 30];
347    /// let valid_office_hours = OfficeHours::try_from((start, finish));
348    /// assert!(valid_office_hours.is_ok());
349    ///
350    /// let start_err: &[u32] = &[100000000];
351    /// let invalid_office_hours = OfficeHours::try_from((start_err, finish));
352    /// assert!(invalid_office_hours.is_err());
353    /// ```
354    fn try_from(office_hours: (&'a [u32], &'a [u32])) -> Result<Self, Self::Error> {
355        let (start, finish) = office_hours;
356        Ok(Self {
357            start: NaiveTime::from_slice_u32(start)?,
358            finish: NaiveTime::from_slice_u32(finish)?,
359        })
360    }
361}
362
363/// Iterator over [`OfficeHours`] that returns the hourly [`NaiveTime`].
364pub struct OfficeHoursIter {
365    start: NaiveTime,
366    finish: NaiveTime,
367}
368
369impl Iterator for OfficeHoursIter {
370    type Item = NaiveTime;
371
372    fn next(&mut self) -> Option<NaiveTime> {
373        if self.start == self.finish {
374            None
375        } else {
376            let current_hour = NaiveTime::from_time_u32(self.start.hour()).unwrap();
377            let is_before_midnight = self.start == NaiveTime::from_time(Clock::ElevenPm);
378            self.start = if is_before_midnight {
379                NaiveTime::default()
380            } else {
381                self.start + Duration::hours(1)
382            };
383            Some(current_hour)
384        }
385    }
386}
387
388impl IntoIterator for &OfficeHours {
389    type Item = NaiveTime;
390    type IntoIter = OfficeHoursIter;
391    fn into_iter(self) -> Self::IntoIter {
392        self.iter()
393    }
394}