Skip to main content

holidays_ru/
range.rs

1use crate::raw_date::RawDate;
2use crate::{DayFlags, Resolved, flags_raw};
3
4/// Норма рабочей недели для расчёта рабочего времени.
5///
6/// Для рабочих дней используются нормы 8, 7.2 и 4.8 часа соответственно.
7/// Сокращённый рабочий день уменьшает норму на 1 час.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum WorkWeek {
10    /// 40-часовая рабочая неделя: 8 часов в рабочий день.
11    FortyHours,
12
13    /// 36-часовая рабочая неделя: 7 часов 12 минут в рабочий день.
14    ThirtySixHours,
15
16    /// 24-часовая рабочая неделя: 4 часа 48 минут в рабочий день.
17    TwentyFourHours,
18}
19
20impl WorkWeek {
21    #[inline]
22    pub(crate) const fn daily_minutes(self) -> u32 {
23        match self {
24            Self::FortyHours => 8 * 60,
25            Self::ThirtySixHours => 7 * 60 + 12,
26            Self::TwentyFourHours => 4 * 60 + 48,
27        }
28    }
29}
30
31/// Считает нерабочие дни в полуоткрытом диапазоне дат `[start, end)`.
32///
33/// Правая граница может быть `MAX_YEAR + 1`-01-01, чтобы диапазон мог
34/// включать последний поддерживаемый день `MAX_YEAR`-12-31. Если хотя бы один
35/// день диапазона рассчитан прогнозом, итоговый результат будет
36/// [`Resolved::Predict`].
37pub(crate) fn non_working_days_between_raw(start: RawDate, end: RawDate) -> Option<Resolved<u32>> {
38    fold_days(start, end, |flags| u32::from(flags.is_day_off()))
39}
40
41/// Считает рабочее время в минутах в полуоткрытом диапазоне дат `[start, end)`.
42///
43/// Нерабочие дни дают 0 минут. Сокращённые рабочие дни уменьшают норму
44/// выбранной рабочей недели на 60 минут.
45pub(crate) fn working_minutes_between_raw(
46    start: RawDate,
47    end: RawDate,
48    week: WorkWeek,
49) -> Option<Resolved<u32>> {
50    fold_days(start, end, |flags| working_minutes_for_day(flags, week))
51}
52
53#[inline]
54fn working_minutes_for_day(flags: DayFlags, week: WorkWeek) -> u32 {
55    if !flags.is_working_day() {
56        return 0;
57    }
58
59    let minutes = week.daily_minutes();
60
61    if flags.is_short_day() {
62        minutes.saturating_sub(60)
63    } else {
64        minutes
65    }
66}
67
68fn fold_days(
69    start: RawDate,
70    end: RawDate,
71    value: impl Fn(DayFlags) -> u32,
72) -> Option<Resolved<u32>> {
73    if !start.is_supported() || !end.is_supported_range_end() || start > end {
74        return None;
75    }
76
77    let mut date = start;
78    let mut total = 0u32;
79    let mut has_predict = false;
80
81    while date < end {
82        let resolved = flags_raw(date);
83
84        has_predict |= resolved.is_predict();
85        total = total.saturating_add(value(resolved.value()));
86        date = date.next_day();
87    }
88
89    if has_predict {
90        Some(Resolved::Predict(total))
91    } else {
92        Some(Resolved::Fact(total))
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_working_minutes_for_short_days() {
102        let flags = DayFlags::WORKING_DAY.with(DayFlags::SHORT_DAY);
103
104        assert_eq!(working_minutes_for_day(flags, WorkWeek::FortyHours), 420);
105        assert_eq!(
106            working_minutes_for_day(flags, WorkWeek::ThirtySixHours),
107            372
108        );
109        assert_eq!(
110            working_minutes_for_day(flags, WorkWeek::TwentyFourHours),
111            228
112        );
113    }
114
115    #[test]
116    fn test_year_totals_2003_2026() {
117        for (year, calendar_days, working_days, non_working_days, h40, h36, h24) in [
118            (2003, 365, 250, 115, 1992 * 60, 1792 * 60, 1192 * 60),
119            (
120                2004,
121                366,
122                251,
123                115,
124                2004 * 60,
125                1803 * 60 + 12,
126                1200 * 60 + 48,
127            ),
128            (
129                2005,
130                365,
131                248,
132                117,
133                1981 * 60,
134                1782 * 60 + 36,
135                1187 * 60 + 24,
136            ),
137            (
138                2006,
139                365,
140                248,
141                117,
142                1980 * 60,
143                1781 * 60 + 36,
144                1186 * 60 + 24,
145            ),
146            (
147                2007,
148                365,
149                249,
150                116,
151                1986 * 60,
152                1786 * 60 + 48,
153                1189 * 60 + 12,
154            ),
155            (2008, 366, 250, 116, 1993 * 60, 1793 * 60, 1193 * 60),
156            (
157                2009,
158                365,
159                249,
160                116,
161                1987 * 60,
162                1787 * 60 + 48,
163                1190 * 60 + 12,
164            ),
165            (
166                2010,
167                365,
168                249,
169                116,
170                1987 * 60,
171                1787 * 60 + 48,
172                1190 * 60 + 12,
173            ),
174            (
175                2011,
176                365,
177                248,
178                117,
179                1981 * 60,
180                1782 * 60 + 36,
181                1187 * 60 + 24,
182            ),
183            (
184                2012,
185                366,
186                249,
187                117,
188                1986 * 60,
189                1786 * 60 + 48,
190                1189 * 60 + 12,
191            ),
192            (
193                2013,
194                365,
195                247,
196                118,
197                1970 * 60,
198                1772 * 60 + 24,
199                1179 * 60 + 36,
200            ),
201            (
202                2014,
203                365,
204                247,
205                118,
206                1970 * 60,
207                1772 * 60 + 24,
208                1179 * 60 + 36,
209            ),
210            (
211                2015,
212                365,
213                247,
214                118,
215                1971 * 60,
216                1773 * 60 + 24,
217                1180 * 60 + 36,
218            ),
219            (
220                2016,
221                366,
222                247,
223                119,
224                1974 * 60,
225                1776 * 60 + 24,
226                1183 * 60 + 36,
227            ),
228            (
229                2017,
230                365,
231                247,
232                118,
233                1973 * 60,
234                1775 * 60 + 24,
235                1182 * 60 + 36,
236            ),
237            (
238                2018,
239                365,
240                247,
241                118,
242                1970 * 60,
243                1772 * 60 + 24,
244                1179 * 60 + 36,
245            ),
246            (
247                2019,
248                365,
249                247,
250                118,
251                1970 * 60,
252                1772 * 60 + 24,
253                1179 * 60 + 36,
254            ),
255            (
256                2020,
257                366,
258                248,
259                118,
260                1979 * 60,
261                1780 * 60 + 36,
262                1185 * 60 + 24,
263            ),
264            (
265                2021,
266                365,
267                247,
268                118,
269                1972 * 60,
270                1774 * 60 + 24,
271                1181 * 60 + 36,
272            ),
273            (
274                2022,
275                365,
276                247,
277                118,
278                1973 * 60,
279                1775 * 60 + 24,
280                1182 * 60 + 36,
281            ),
282            (
283                2023,
284                365,
285                247,
286                118,
287                1973 * 60,
288                1775 * 60 + 24,
289                1182 * 60 + 36,
290            ),
291            (
292                2024,
293                366,
294                248,
295                118,
296                1979 * 60,
297                1780 * 60 + 36,
298                1185 * 60 + 24,
299            ),
300            (
301                2025,
302                365,
303                247,
304                118,
305                1972 * 60,
306                1774 * 60 + 24,
307                1181 * 60 + 36,
308            ),
309            (
310                2026,
311                365,
312                247,
313                118,
314                1972 * 60,
315                1774 * 60 + 24,
316                1181 * 60 + 36,
317            ),
318        ] {
319            let start = RawDate::from_ymd(year, 1, 1).expect("valid start date");
320            let end = RawDate::from_ymd(year + 1, 1, 1).expect("valid end date");
321
322            let non_working = non_working_days_between_raw(start, end).expect("valid range");
323            assert_eq!(non_working, Resolved::Fact(non_working_days), "{year}");
324            assert_eq!(calendar_days - non_working.value(), working_days, "{year}");
325
326            assert_eq!(
327                working_minutes_between_raw(start, end, WorkWeek::FortyHours),
328                Some(Resolved::Fact(h40)),
329                "{year}"
330            );
331            assert_eq!(
332                working_minutes_between_raw(start, end, WorkWeek::ThirtySixHours),
333                Some(Resolved::Fact(h36)),
334                "{year}"
335            );
336            assert_eq!(
337                working_minutes_between_raw(start, end, WorkWeek::TwentyFourHours),
338                Some(Resolved::Fact(h24)),
339                "{year}"
340            );
341        }
342    }
343
344    #[test]
345    fn test_empty_range_is_fact_zero() {
346        let date = RawDate::from_ymd(2026, 1, 1).expect("valid date");
347
348        assert_eq!(
349            non_working_days_between_raw(date, date),
350            Some(Resolved::Fact(0))
351        );
352        assert_eq!(
353            working_minutes_between_raw(date, date, WorkWeek::FortyHours),
354            Some(Resolved::Fact(0))
355        );
356    }
357
358    #[test]
359    fn test_reversed_range_is_none() {
360        let start = RawDate::from_ymd(2026, 1, 2).expect("valid start date");
361        let end = RawDate::from_ymd(2026, 1, 1).expect("valid end date");
362
363        assert_eq!(non_working_days_between_raw(start, end), None);
364        assert_eq!(
365            working_minutes_between_raw(start, end, WorkWeek::FortyHours),
366            None
367        );
368    }
369
370    #[test]
371    fn test_mixed_fact_predict_range_is_predict() {
372        let start = RawDate::from_ymd(2026, 12, 31).expect("valid start date");
373        let end = RawDate::from_ymd(2027, 1, 2).expect("valid end date");
374
375        assert_eq!(
376            non_working_days_between_raw(start, end),
377            Some(Resolved::Predict(2))
378        );
379    }
380
381    #[test]
382    fn test_range_can_include_last_supported_day() {
383        let start = RawDate::from_ymd(crate::MAX_YEAR, 12, 31).expect("valid start date");
384        let end = RawDate::from_ymd_unchecked(crate::MAX_YEAR + 1, 1, 1);
385
386        assert!(non_working_days_between_raw(start, end).is_some());
387        assert!(working_minutes_between_raw(start, end, WorkWeek::FortyHours).is_some());
388    }
389}