1pub 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
11pub trait Off {
14 fn is_off_time(&self) -> bool;
16}
17
18#[derive(Serialize, Deserialize, Debug)]
21pub enum Parity {
22 EveryWeek,
24 OddWeek,
26 EvenWeek,
28}
29
30#[derive(Serialize, Deserialize, Debug)]
32#[serde(transparent)]
33pub struct OffDays(HashMap<Weekday, Parity>);
34
35struct Time {}
36
37#[cfg_attr(test, automock)] pub trait Now {
43 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 pub fn new() -> OffDays {
55 OffDays(HashMap::new())
56 }
57 #[allow(dead_code)]
58 fn insert(&mut self, day: Weekday, parity: Parity) -> Option<Parity> {
60 self.0.insert(day, parity)
61 }
62 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 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 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; #[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}