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}