lib/
offtime.rs

1//! This module Provide the [`Off`] trait and [`OffDays`] struct
2pub use chrono::Weekday;
3use chrono::{Date, Datelike, Local};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, trace};
7
8#[cfg(test)]
9use mockall::automock;
10
11/// Manage the time where the application shall not update the status because the user
12/// is not working
13pub trait Off {
14    /// Is the user off now ?
15    fn is_off_time(&self) -> bool;
16}
17
18/// Struct for describing the parity of the week for which the out of work day apply
19/// Parity is given according to iso week number
20#[derive(Serialize, Deserialize, Debug)]
21pub enum Parity {
22    /// Day off for all weeks
23    EveryWeek,
24    /// Day off only for odd weeks
25    OddWeek,
26    /// Day off only for even weeks
27    EvenWeek,
28}
29
30/// Struct olding a map of ([`Weekday`], [`Parity`]) descripting day offs.
31#[derive(Serialize, Deserialize, Debug)]
32#[serde(transparent)]
33pub struct OffDays(HashMap<Weekday, Parity>);
34
35struct Time {}
36
37/// Trait providing a `now` function.
38///
39/// The use of a trait instead of calling directly `Local::now` is needed in order to be able to
40/// mock time in tests
41#[cfg_attr(test, automock)] // create MockNow Struct for tests
42pub trait Now {
43    /// Returns current local time
44    fn now(&self) -> Date<Local>;
45}
46impl Now for Time {
47    fn now(&self) -> Date<Local> {
48        Local::now().date()
49    }
50}
51
52impl OffDays {
53    /// Create new empty `OffDays` instance
54    pub fn new() -> OffDays {
55        OffDays(HashMap::new())
56    }
57    #[allow(dead_code)]
58    /// Insert a new offday for week of `parity`
59    fn insert(&mut self, day: Weekday, parity: Parity) -> Option<Parity> {
60        self.0.insert(day, parity)
61    }
62    /// The user is off if date day is in OffDays and either,
63    /// - parity is all
64    /// - parity match the current iso week number
65    fn is_off_at_date(&self, date: impl Now) -> bool {
66        let now = date.now();
67        trace!("now: {:?}", now);
68        trace!("now.weekday: {:?}", now.weekday());
69        let res: bool;
70        if let Some(parity) = self.0.get(&now.weekday()) {
71            trace!("match and parity = {:?}", parity);
72            res = match parity {
73                Parity::EveryWeek => true,
74                Parity::OddWeek => &now.iso_week().week() % 2 == 1,
75                Parity::EvenWeek => &now.iso_week().week() % 2 == 0,
76            };
77        } else {
78            res = false;
79        }
80        debug!(
81            "{:?} {:?} is {} off",
82            &now.weekday(),
83            &now.iso_week(),
84            if !res { "not" } else { "" }
85        );
86        res
87    }
88
89    /// Return `true` if there are no OffDays.
90    pub fn is_empty(&self) -> bool {
91        self.0.is_empty()
92    }
93}
94
95impl Default for OffDays {
96    fn default() -> Self {
97        OffDays::new()
98    }
99}
100
101impl Off for OffDays {
102    /// The user is off if
103    /// current day is in OffDays and either,
104    /// - parity is all
105    /// - parity match the current iso week number
106    fn is_off_time(&self) -> bool {
107        self.is_off_at_date(Time {})
108    }
109}
110
111#[cfg(test)]
112mod is_off_should {
113    use super::*;
114    use anyhow::Result;
115    use chrono::{Local, TimeZone, Weekday};
116    use test_log::test; // Automatically trace tests
117
118    #[test]
119    fn return_false_when_day_dont_match() -> Result<()> {
120        let mut leave = OffDays::new();
121        leave.insert(Weekday::Mon, Parity::EveryWeek);
122        let mut mock = MockNow::new();
123        mock.expect_now()
124            .times(1)
125            .returning(|| Local.isoywd(2015, 1, Weekday::Tue));
126        assert_eq!(leave.is_off_at_date(mock), false);
127        Ok(())
128    }
129
130    #[test]
131    fn return_true_when_match_and_no_parity() -> Result<()> {
132        let mut leave = OffDays::new();
133        leave.insert(Weekday::Tue, Parity::EveryWeek);
134        let mut mock = MockNow::new();
135        mock.expect_now()
136            .times(1)
137            .returning(|| Local.isoywd(2015, 1, Weekday::Tue));
138        assert_eq!(leave.is_off_at_date(mock), true);
139        Ok(())
140    }
141
142    #[test]
143    fn return_true_when_day_and_parity_match() -> Result<()> {
144        let mut leave = OffDays::new();
145        leave.insert(Weekday::Wed, Parity::OddWeek);
146
147        let mut mock = MockNow::new();
148        mock.expect_now()
149            .times(1)
150            .returning(|| Local.isoywd(2015, 15, Weekday::Wed));
151        assert_eq!(leave.is_off_at_date(mock), true);
152
153        leave.insert(Weekday::Thu, Parity::EvenWeek);
154        let mut mock = MockNow::new();
155        mock.expect_now()
156            .times(1)
157            .returning(|| Local.isoywd(2015, 16, Weekday::Thu));
158        assert_eq!(leave.is_off_at_date(mock), true);
159
160        Ok(())
161    }
162
163    #[test]
164    fn return_false_when_day_match_but_not_parity() -> Result<()> {
165        let mut leave = OffDays::new();
166        leave.insert(Weekday::Fri, Parity::EvenWeek);
167        let mut mock = MockNow::new();
168        mock.expect_now()
169            .times(1)
170            .returning(|| Local.isoywd(2015, 15, Weekday::Fri));
171        assert_eq!(leave.is_off_at_date(mock), false);
172
173        leave.insert(Weekday::Sun, Parity::OddWeek);
174        let mut mock = MockNow::new();
175        mock.expect_now()
176            .times(1)
177            .returning(|| Local.isoywd(2015, 16, Weekday::Sun));
178        assert_eq!(leave.is_off_at_date(mock), false);
179        Ok(())
180    }
181}