chrono_datepicker_core/config/
date_constraints.rs

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/// Trait that can be implemented to create your own date constraints.
12#[cfg_attr(test, automock)]
13pub trait HasDateConstraints {
14    /// Returns true if the given date is forbidden.
15    fn is_day_forbidden(&self, date: &NaiveDate) -> bool;
16
17    /// Returns true if the entire month described by year_month_info is forbidden.
18    fn is_month_forbidden(&self, year_month_info: &NaiveDate) -> bool;
19
20    /// Returns true if the entire given year is forbidden.
21    fn is_year_forbidden(&self, year: i32) -> bool;
22
23    /// Returns true if the entire group of years including the given year is forbidden.
24    /// A group of years are inclusive intervals [1980, 1999], [2000, 2019], [2020, 2039], ...
25    fn is_year_group_forbidden(&self, year: i32) -> bool;
26}
27
28/// Date constraints configuration
29#[derive(Default, Debug, Clone, Builder)]
30#[builder(setter(strip_option))]
31#[builder(default)]
32#[builder(build_fn(validate = "Self::validate"))]
33pub struct DateConstraints {
34    /// inclusive minimal date constraint
35    /// the earliest date that can be selected
36    min_date: Option<NaiveDate>,
37
38    /// inclusive maximal date constraint
39    /// the latest date that can be selected
40    max_date: Option<NaiveDate>,
41
42    /// disabled weekdays, that should not be selectable
43    disabled_weekdays: HashSet<Weekday>,
44
45    /// entire completely disabled months in every year
46    disabled_months: HashSet<Month>,
47
48    /// entire completely disabled years
49    disabled_years: HashSet<i32>,
50
51    /// disabled monthly periodically repeating dates, so it is just a day number
52    /// starting from 1 for the first day of the month
53    /// if unique dates in a certain year should not be selectable use `disabled_unique_dates`
54    disabled_monthly_dates: HashSet<u32>,
55
56    /// disabled yearly periodically repeating dates that should not be selectable,
57    /// if unique dates in a certain year should not be selectable use `disabled_unique_dates`
58    /// it is a `Vec` since we need to iterate over it anyway, since we hae no MonthDay type
59    disabled_yearly_dates: Vec<NaiveDate>,
60
61    /// disabled unique dates with a specific year, month and day that should not be selectable,
62    /// if some periodically repeated dates should not be selectable use the correct option
63    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
80// TODO: find out how to place #[derive(Debug, Clone)] on the structure generated by automock
81// this is a temporary workaround for tests
82cfg_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, //
151        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, //
160        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, //
169        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}