time_range_ext/
working_hours.rs

1use std::ops::{Add, Sub};
2use time::{OffsetDateTime, Time, Weekday};
3use time::ext::NumericalDuration;
4use crate::TimeRange;
5
6#[derive(Debug, Clone)]
7pub struct WorkingHours {
8   pub start: Time,
9   pub end: Time,
10   pub active_days: Vec<Weekday>,
11
12   pub lower_bound: Option<OffsetDateTime>,
13   pub upper_bound: Option<OffsetDateTime>
14}
15
16impl WorkingHours {
17    pub fn is_active(&self) -> bool {
18        !self.active_days.is_empty()
19    }
20
21    pub fn active_during_ts(&self, time: OffsetDateTime) -> bool {
22        self.active_days.contains(&time.weekday()) && self.start <= time.time() && time.time() <= self.end
23    }
24
25    pub fn active_during_day(&self, day: Weekday) -> bool {
26        self.active_days.contains(&day)
27    }
28
29    pub fn working_time_in_range(&self, range: TimeRange) -> Vec<TimeRange> {
30        if !self.is_active() {
31            return vec![];
32        }
33
34        let range_start = range.start;
35        let range_end = range.end;
36        
37        let mut working_times = vec![];
38        let mut current = range_start;
39
40        if let Some(previous_working_hours) = self.previous_working_hours(current) {
41            if range_start <= previous_working_hours.end {
42                working_times.push(previous_working_hours);
43            }
44        }
45
46        while current < range_end {
47            if !self.active_during_day(current.weekday()) {
48                current = current.add(1.days());
49                continue;
50            }
51
52            let next_shift = self.next_working_hours(current);
53
54            if let Some(next_working_hours) = next_shift {
55                if next_working_hours.start <= range_end && !working_times.contains(&next_working_hours) {
56                    working_times.push(next_working_hours);
57                }
58            }
59
60            current = current.add(1.days());
61        }
62
63        if let Some(first) = working_times.first_mut() {
64            if first.start < range_start {
65                first.start = range_start;
66            }
67        }
68
69        if let Some(last) = working_times.last_mut() {
70            if last.end > range_end {
71                last.end = range_end;
72            }
73        }
74
75        working_times
76    }
77
78    fn exceeds_bounds(&self, ts: OffsetDateTime) -> bool {
79        self.lower_bound.map_or(false, |l| ts < l) || self.upper_bound.map_or(false, |u| ts > u)
80    }
81
82    fn previous_working_hours(&self, ts: OffsetDateTime) -> Option<TimeRange> {
83        let mut current = ts;
84
85        if !self.is_active() {
86            return None;
87        }
88
89        if self.exceeds_bounds(current) {
90            return None;
91        }
92
93        // handle case that the requested timestamp lies before start and end time, but would be
94        // active on that day, and would therefore cause times that are invalid
95        if self.start > ts.time() {
96            current = current.sub(1.days());
97        }
98
99        while !self.active_during_day(current.weekday()) {
100            current = current.sub(1.days());
101        }
102
103        // handle Case when we have a working time that has a start time of e.g., 23:30 and end time of 03:00
104        if self.start > self.end {
105            let start_date = current.replace_time(self.start);
106            let mut end_date = current.replace_time(self.end);
107
108            if end_date < start_date {
109                end_date = end_date.add(1.days());
110            }
111
112            // always check if the replaced date of the working time is set,
113            // and the end date would be greater than our current end_date
114            if self.upper_bound.map_or(false, |r| end_date > r) {
115                end_date = self.upper_bound.unwrap();
116            }
117
118            return Some(TimeRange {
119                start: start_date,
120                end: end_date,
121            });
122        }
123
124        let mut end = current.replace_time(self.end);
125
126        // the same check as above, but for the normal case where times are within the same day
127        if self.upper_bound.map_or(false, |r| end > r) {
128            end = self.upper_bound.unwrap();
129        }
130
131        let mut start = current.replace_time(self.start);
132
133        if self.lower_bound.map_or(false, |l| start < l) {
134            start = self.lower_bound.unwrap();
135        }
136
137        Some(TimeRange { start, end })
138    }
139
140    fn next_working_hours(&self, ts: OffsetDateTime) -> Option<TimeRange> {
141        let mut current = ts;
142
143        if !self.is_active() {
144            return None;
145        }
146
147        if self.exceeds_bounds(current) {
148            return None;
149        }
150
151        if current.time() > self.start && current.time() > self.end {
152            current = current.add(1.days());
153        }
154
155        while !self.active_during_day(current.weekday()) {
156            current = current.add(time::Duration::days(1));
157        }
158
159        if self.start > self.end {
160            let start_date = current.replace_time(self.start);
161            let mut end_date = current.replace_time(self.end).add(1.days());
162
163            // always check if the replaced date of the times is set,
164            // and the end date would be greater than our current end_date
165            if self.upper_bound.map_or(false, |r| end_date > r) {
166                end_date = self.upper_bound.unwrap();
167            }
168
169            if end_date < start_date {
170                end_date = end_date.add(1.days());
171            }
172
173            return Some(TimeRange {
174                start: start_date,
175                end: end_date,
176            });
177        }
178
179        let mut end = current.replace_time(self.end);
180        if self.upper_bound.map_or(false, |r| end > r) {
181            end = self.upper_bound.unwrap();
182        }
183
184        let mut start = current.replace_time(self.start);
185
186        if self.lower_bound.map_or(false, |l| start < l) {
187            start = self.lower_bound.unwrap();
188        }
189
190        if start > end {
191            return None;
192        }
193
194        Some(TimeRange { start, end })
195    }
196}