1use chrono::prelude::*;
2use std::collections::HashSet;
3
4use num_traits::FromPrimitive;
5
6use crate::viewed_date::{year_group_range, ViewedDate};
7
8#[cfg(test)]
9use mockall::automock;
10
11#[cfg_attr(test, automock)]
13pub trait HasDateConstraints {
14 fn is_day_forbidden(&self, date: &NaiveDate) -> bool;
16
17 fn is_month_forbidden(&self, year_month_info: &NaiveDate) -> bool;
19
20 fn is_year_forbidden(&self, year: i32) -> bool;
22
23 fn is_year_group_forbidden(&self, year: i32) -> bool;
26}
27
28#[derive(Default, Debug, Clone, Builder)]
30#[builder(setter(strip_option))]
31#[builder(default)]
32#[builder(build_fn(validate = "Self::validate"))]
33pub struct DateConstraints {
34 min_date: Option<NaiveDate>,
37
38 max_date: Option<NaiveDate>,
41
42 disabled_weekdays: HashSet<Weekday>,
44
45 disabled_months: HashSet<Month>,
47
48 disabled_years: HashSet<i32>,
50
51 disabled_monthly_dates: HashSet<u32>,
55
56 disabled_yearly_dates: Vec<NaiveDate>,
60
61 disabled_unique_dates: HashSet<NaiveDate>,
64}
65
66impl DateConstraintsBuilder {
67 fn validate(&self) -> Result<(), String> {
68 match (self.min_date, self.max_date) {
69 (Some(min_date), Some(max_date)) => {
70 if min_date > max_date {
71 return Err("min_date must be earlier or exactly at max_date".into());
72 }
73 }
74 (_, _) => {}
75 }
76 Ok(())
77 }
78}
79
80cfg_if::cfg_if! {
83 if #[cfg(test)] {
84 impl Clone for MockHasDateConstraints {
85 fn clone(&self) -> Self {
86 Self::new()
87 }
88 }
89
90 use core::fmt;
91 impl fmt::Debug for MockHasDateConstraints {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("MockHasDateConstraints").finish()
94 }
95 }
96 }
97}
98
99impl HasDateConstraints for DateConstraints {
100 fn is_day_forbidden(&self, date: &NaiveDate) -> bool {
101 self.min_date.map_or(false, |min_date| &min_date > date)
102 || self.max_date.map_or(false, |max_date| &max_date < date)
103 || self.disabled_weekdays.contains(&date.weekday())
104 || self
105 .disabled_months
106 .contains(&Month::from_u32(date.month()).unwrap())
107 || self.disabled_years.contains(&date.year())
108 || self.disabled_unique_dates.contains(&date)
109 || self.disabled_monthly_dates.contains(&date.day())
110 || self
111 .disabled_yearly_dates
112 .iter()
113 .any(|disabled| disabled.day() == date.day() && disabled.month() == date.month())
114 }
115
116 fn is_month_forbidden(&self, year_month_info: &NaiveDate) -> bool {
117 self.disabled_years.contains(&year_month_info.year())
118 || self
119 .disabled_months
120 .contains(&Month::from_u32(year_month_info.month()).unwrap())
121 || year_month_info
122 .first_day_of_month()
123 .iter_days()
124 .take_while(|date| date.month() == year_month_info.month())
125 .all(|date| self.is_day_forbidden(&date))
126 }
127
128 fn is_year_forbidden(&self, year: i32) -> bool {
129 self.disabled_years.contains(&year)
130 || (1..=12u32)
131 .all(|month| self.is_month_forbidden(&NaiveDate::from_ymd(year, month, 1)))
132 }
133
134 fn is_year_group_forbidden(&self, year: i32) -> bool {
135 year_group_range(year).all(|year| self.is_year_forbidden(year))
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::{
143 rstest_utils::create_date,
144 viewed_date::{DayNumber, MonthNumber, YearNumber},
145 };
146 use chrono::Duration;
147 use rstest::*;
148
149 #[rstest(
150 tested_date, case(create_date(1, 12, 25)),
152 case(create_date(3000, 3, 22)),
153 )]
154 fn is_day_forbidden_default_no_bounds(tested_date: NaiveDate) {
155 assert!(!DateConstraints::default().is_day_forbidden(&tested_date))
156 }
157
158 #[rstest(
159 tested_date, case(create_date(1, 12, 25)),
161 case(create_date(3000, 3, 22)),
162 )]
163 fn is_month_forbidden_default_no_bounds(tested_date: NaiveDate) {
164 assert!(!DateConstraints::default().is_month_forbidden(&tested_date))
165 }
166
167 #[rstest(
168 tested_year, case(1),
170 case(3000),
171 )]
172 fn is_year_forbidden_default_no_bounds(tested_year: YearNumber) {
173 assert!(!DateConstraints::default().is_year_forbidden(tested_year))
174 }
175
176 #[test]
177 fn picker_config_min_date_greater_than_max_date() {
178 let date = NaiveDate::from_ymd(2020, 10, 15);
179 let config = DateConstraintsBuilder::default()
180 .min_date(date.clone())
181 .max_date(date.clone() - Duration::days(1))
182 .build();
183 assert!(config.is_err());
184 assert_eq!(
185 config.unwrap_err().to_string(),
186 "min_date must be earlier or exactly at max_date"
187 );
188 }
189
190 #[test]
191 fn picker_config_min_date_equals_max_date() {
192 let date = NaiveDate::from_ymd(2020, 10, 15);
193 let config = DateConstraintsBuilder::default()
194 .min_date(date.clone())
195 .max_date(date.clone())
196 .build();
197 assert!(config.is_ok());
198 }
199
200 #[test]
201 fn is_day_forbidden_at_min_date_allowed() {
202 let date = NaiveDate::from_ymd(2020, 10, 15);
203 let config = DateConstraintsBuilder::default()
204 .min_date(date.clone())
205 .build()
206 .unwrap();
207 assert!(!config.is_day_forbidden(&date))
208 }
209
210 #[test]
211 fn is_day_forbidden_before_min_date_not_allowed() {
212 let date = NaiveDate::from_ymd(2020, 10, 15);
213 let config = DateConstraintsBuilder::default()
214 .min_date(date.clone())
215 .build()
216 .unwrap();
217 assert!(config.is_day_forbidden(&(date - Duration::days(1))))
218 }
219
220 #[test]
221 fn is_day_forbidden_at_max_date_allowed() {
222 let date = NaiveDate::from_ymd(2020, 10, 15);
223 let config = DateConstraintsBuilder::default()
224 .max_date(date.clone())
225 .build()
226 .unwrap();
227 assert!(!config.is_day_forbidden(&date))
228 }
229
230 #[test]
231 fn is_day_forbidden_after_max_date_not_allowed() {
232 let date = NaiveDate::from_ymd(2020, 10, 15);
233 let config = DateConstraintsBuilder::default()
234 .max_date(date.clone())
235 .build()
236 .unwrap();
237 assert!(config.is_day_forbidden(&(date + Duration::days(1))))
238 }
239
240 #[rstest(
241 year => [1, 2000, 3000],
242 week => [1, 25, 51],
243 disabled_weekday => [Weekday::Mon, Weekday::Tue, Weekday::Sat],
244 )]
245 fn is_day_forbidden_disabled_weekday_not_allowed(
246 year: YearNumber,
247 week: u32,
248 disabled_weekday: Weekday,
249 ) {
250 let config = DateConstraintsBuilder::default()
251 .disabled_weekdays([disabled_weekday].iter().cloned().collect())
252 .build()
253 .unwrap();
254 assert!(config.is_day_forbidden(&NaiveDate::from_isoywd(year, week, disabled_weekday)));
255 }
256
257 #[rstest(
258 year => [1, 2000, 3000],
259 disabled_month => [Month::January, Month::July, Month::December],
260 day => [1, 15, 27],
261 )]
262 fn is_day_forbidden_disabled_month_not_allowed(
263 year: YearNumber,
264 disabled_month: Month,
265 day: DayNumber,
266 ) {
267 let config = DateConstraintsBuilder::default()
268 .disabled_months([disabled_month].iter().cloned().collect())
269 .build()
270 .unwrap();
271 assert!(config.is_day_forbidden(&NaiveDate::from_ymd(
272 year,
273 disabled_month.number_from_month(),
274 day
275 )))
276 }
277
278 #[rstest(
279 disabled_year => [1, 2000, 3000],
280 month => [1, 7, 12],
281 day => [1, 15, 27],
282 )]
283 fn is_day_forbidden_disabled_year_not_allowed(
284 disabled_year: YearNumber,
285 month: MonthNumber,
286 day: DayNumber,
287 ) {
288 let config = DateConstraintsBuilder::default()
289 .disabled_years([disabled_year].iter().cloned().collect())
290 .build()
291 .unwrap();
292 assert!(config.is_day_forbidden(&NaiveDate::from_ymd(disabled_year, month, day)))
293 }
294
295 #[test]
296 fn is_day_forbidden_disabled_unique_dates_not_allowed() {
297 let date = NaiveDate::from_ymd(2020, 1, 16);
298 let config = DateConstraintsBuilder::default()
299 .disabled_unique_dates([date].iter().cloned().collect())
300 .build()
301 .unwrap();
302 assert!(config.is_day_forbidden(&date))
303 }
304
305 #[test]
306 fn is_day_forbidden_disabled_unique_dates_after_a_year_allowed() {
307 let date = NaiveDate::from_ymd(2020, 1, 16);
308 let config = DateConstraintsBuilder::default()
309 .disabled_unique_dates([date].iter().cloned().collect())
310 .build()
311 .unwrap();
312 assert!(!config.is_day_forbidden(&NaiveDate::from_ymd(2021, 1, 16)))
313 }
314
315 #[rstest(
316 year_in_disabled => [1, 2000, 3000],
317 year_in_input => [1, 1500, 2000],
318 month => [1, 7, 12],
319 day => [1, 15, 27],
320 )]
321 fn is_day_forbidden_disabled_yearly_dates_not_allowed(
322 year_in_disabled: YearNumber,
323 year_in_input: YearNumber,
324 month: MonthNumber,
325 day: DayNumber,
326 ) {
327 let disabled_yearly_date = NaiveDate::from_ymd(year_in_disabled, month, day);
328 let config = DateConstraintsBuilder::default()
329 .disabled_yearly_dates(vec![disabled_yearly_date])
330 .build()
331 .unwrap();
332 assert!(config.is_day_forbidden(&NaiveDate::from_ymd(year_in_input, month, day)))
333 }
334
335 #[rstest(
336 year => [1, 2000, 3000],
337 month => [1, 7, 12],
338 day => [1, 15, 27],
339 )]
340 fn is_day_forbidden_disabled_monthly_dates_not_allowed(
341 year: YearNumber,
342 month: MonthNumber,
343 day: DayNumber,
344 ) {
345 let config = DateConstraintsBuilder::default()
346 .disabled_monthly_dates([day].iter().cloned().collect())
347 .build()
348 .unwrap();
349 assert!(config.is_day_forbidden(&NaiveDate::from_ymd(year, month, day)))
350 }
351
352 #[rstest(
353 year => [1, 2000, 3000],
354 disabled_month => [Month::January, Month::July, Month::December],
355 day => [1, 15, 27],
356 )]
357 fn is_month_forbidden_disabled_months_not_allowed(
358 year: YearNumber,
359 disabled_month: Month,
360 day: DayNumber,
361 ) {
362 let config = DateConstraintsBuilder::default()
363 .disabled_months([disabled_month].iter().cloned().collect())
364 .build()
365 .unwrap();
366 assert!(config.is_month_forbidden(&NaiveDate::from_ymd(
367 year,
368 disabled_month.number_from_month(),
369 day
370 )))
371 }
372
373 #[rstest(
374 disabled_year => [1, 2000, 3000],
375 month => [1, 7, 12],
376 day => [1, 15, 27],
377 )]
378 fn is_month_forbidden_disabled_years_not_allowed(
379 disabled_year: YearNumber,
380 month: MonthNumber,
381 day: DayNumber,
382 ) {
383 let config = DateConstraintsBuilder::default()
384 .disabled_years([disabled_year].iter().cloned().collect())
385 .build()
386 .unwrap();
387 assert!(config.is_month_forbidden(&NaiveDate::from_ymd(disabled_year, month, day)))
388 }
389
390 #[rstest(
391 disabled_year => [1, 2000, 3000],
392 )]
393 fn is_year_forbidden_disabled_years_not_allowed(disabled_year: YearNumber) {
394 let config = DateConstraintsBuilder::default()
395 .disabled_years([disabled_year].iter().cloned().collect())
396 .build()
397 .unwrap();
398 assert!(config.is_year_forbidden(disabled_year))
399 }
400
401 #[rstest(
402 disabled_year_group => [1, 2000, 3000],
403 )]
404 fn is_year_group_forbidden_disabled_years_not_allowed(disabled_year_group: YearNumber) {
405 let config = DateConstraintsBuilder::default()
406 .disabled_years(year_group_range(disabled_year_group).collect())
407 .build()
408 .unwrap();
409 assert!(config.is_year_group_forbidden(disabled_year_group))
410 }
411}