evobench_tools/date_and_time/
time_ranges.rs1use std::{fmt::Display, str::FromStr};
5
6use anyhow::{anyhow, bail};
7use chrono::{DateTime, Days, Local, NaiveDate, TimeZone};
8
9use crate::{debug, serde_types::date_and_time::LocalNaiveTime};
10
11pub struct LocalNaiveTimeRange {
12 pub from: LocalNaiveTime,
13 pub to: LocalNaiveTime,
14}
15
16impl FromStr for LocalNaiveTimeRange {
17 type Err = anyhow::Error;
18
19 fn from_str(s: &str) -> Result<Self, Self::Err> {
20 let parts: Vec<&str> = s.split('-').collect();
21 match *parts.as_slice() {
22 [from, to] => {
23 let from = from.trim();
24 let to = to.trim();
25 let from = from
26 .parse()
27 .map_err(|e| anyhow!("from time {from:?}: {e:#}"))?;
28 let to = to.parse().map_err(|e| anyhow!("to time {to:?}: {e:#}"))?;
29 Ok(LocalNaiveTimeRange { from, to })
30 }
31 [_] => {
32 bail!("expecting exactly one '-', none given")
33 }
34 _ => {
35 bail!("expecting exactly one '-', more than one given")
36 }
37 }
38 }
39}
40
41impl From<(LocalNaiveTime, LocalNaiveTime)> for LocalNaiveTimeRange {
42 fn from((from, to): (LocalNaiveTime, LocalNaiveTime)) -> Self {
43 Self { from, to }
44 }
45}
46
47impl From<&(LocalNaiveTime, LocalNaiveTime)> for LocalNaiveTimeRange {
48 fn from((from, to): &(LocalNaiveTime, LocalNaiveTime)) -> Self {
49 Self {
50 from: *from,
51 to: *to,
52 }
53 }
54}
55
56impl From<(&LocalNaiveTime, &LocalNaiveTime)> for LocalNaiveTimeRange {
57 fn from((from, to): (&LocalNaiveTime, &LocalNaiveTime)) -> Self {
58 Self {
59 from: *from,
60 to: *to,
61 }
62 }
63}
64
65impl LocalNaiveTimeRange {
66 pub fn crosses_day_boundary(&self) -> bool {
67 let Self { from, to } = self;
68 to < from
69 }
70
71 pub fn with_start_date_as_unambiguous_locals(
84 &self,
85 nd: NaiveDate,
86 ) -> Option<DateTimeRange<Local>> {
87 let Self { from, to } = self;
88 let from = from.with_date_as_unambiguous_local(nd)?;
89 let nd_end = if self.crosses_day_boundary() {
90 nd.checked_add_days(Days::new(1))?
91 } else {
92 nd
93 };
94 let to = to.with_date_as_unambiguous_local(nd_end)?;
95 Some(DateTimeRange { from, to })
96 }
97
98 pub fn after_datetime(
108 &self,
109 datetime: &DateTime<Local>,
110 allow_time_inside_range: bool,
111 ) -> Option<DateTimeRange<Local>> {
112 debug!("after_datetime({self}, {datetime}, {allow_time_inside_range}):");
113
114 let Self { from, to } = self;
115
116 let dtr_from_nd = |nd: NaiveDate| -> Option<_> {
117 Some(DateTimeRange {
118 from: from.with_date_as_unambiguous_local(nd)?,
119 to: if self.crosses_day_boundary() {
120 to.with_date_as_unambiguous_local(nd.checked_add_days(Days::new(1))?)?
121 } else {
122 to.with_date_as_unambiguous_local(nd)?
123 },
124 })
125 };
126
127 let nd = datetime.date_naive();
129 let dtr_today = dtr_from_nd(nd)?;
130 let contains = dtr_today.contains(datetime);
131 debug!(" dtr_today={dtr_today}, dtr_today.contains(datetime) = {contains}");
132 if contains {
133 if allow_time_inside_range {
134 debug!(" allowed inside -> dtr_today = {dtr_today}");
135 Some(dtr_today)
136 } else {
137 let correct = dtr_from_nd(nd.checked_add_days(Days::new(1))?)?;
138 debug!(" !allowed inside -> next day = {correct}");
139 Some(correct)
140 }
141 } else {
142 let range_is_in_past = dtr_today.to <= *datetime;
143 debug!(" dtr_today.to <= datetime == {range_is_in_past}");
144 if range_is_in_past {
145 let next_day = dtr_from_nd(nd.checked_add_days(Days::new(1))?)?;
147 debug!(" next_day = {next_day}");
148 if !allow_time_inside_range && next_day.contains(datetime) {
149 todo!()
150 } else {
151 Some(next_day)
152 }
153 } else {
154 assert!(*datetime < dtr_today.from);
155 let prev_day = dtr_from_nd(nd.checked_sub_days(Days::new(1))?)?;
158 debug!(" prev_day = {prev_day}");
159 if prev_day.contains(datetime) {
160 if allow_time_inside_range {
161 Some(prev_day)
162 } else {
163 Some(dtr_today)
164 }
165 } else {
166 if *datetime < prev_day.from {
167 Some(prev_day)
168 } else {
169 Some(dtr_today)
170 }
171 }
172 }
173 }
174 }
175}
176
177impl Display for LocalNaiveTimeRange {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 let Self { from, to } = self;
180 write!(f, "{from} - {to}")
181 }
182}
183
184#[derive(Debug, Clone)]
185pub struct DateTimeRange<Tz: TimeZone> {
186 pub from: DateTime<Tz>,
187 pub to: DateTime<Tz>,
188}
189
190impl<Tz: TimeZone> PartialEq for DateTimeRange<Tz> {
191 fn eq(&self, other: &Self) -> bool {
192 self.from.eq(&other.from) && self.to.eq(&other.to)
193 }
194}
195
196impl<Tz: TimeZone> Eq for DateTimeRange<Tz> {}
199
200impl<Tz: TimeZone> Display for DateTimeRange<Tz> {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 let Self { from, to } = self;
203 write!(f, "{} - {}", from.to_rfc3339(), to.to_rfc3339())
204 }
205}
206
207impl<Tz: TimeZone> DateTimeRange<Tz> {
208 pub fn contains(&self, time: &DateTime<Tz>) -> bool {
209 let Self { from, to } = self;
210 from <= time && time < to
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use anyhow::Result;
217
218 use super::*;
219
220 fn naivedate_to_locals(s: &str, y: i32, m: u32, d: u32) -> Result<DateTimeRange<Local>> {
221 let ltr = LocalNaiveTimeRange::from_str(s)?;
222 ltr.with_start_date_as_unambiguous_locals(NaiveDate::from_ymd_opt(y, m, d).unwrap())
223 .ok_or_else(|| anyhow!("ambiguous: {ltr}"))
224 }
225
226 #[test]
227 fn t_with_start_date_as_unambiguous_locals() -> Result<()> {
228 assert_eq!(
229 naivedate_to_locals("2:00-6:00", 2024, 10, 23)?.to_string(),
230 "2024-10-23T02:00:00+02:00 - 2024-10-23T06:00:00+02:00"
231 );
232 assert_eq!(
233 naivedate_to_locals("23:00-6:00", 2024, 10, 23)?.to_string(),
234 "2024-10-23T23:00:00+02:00 - 2024-10-24T06:00:00+02:00"
235 );
236 Ok(())
237 }
238
239 fn datetime_to_locals(
240 s: &str,
241 datetime_str: &str,
242 allow_time_inside_range: bool,
243 ) -> Result<DateTimeRange<Local>> {
244 let datetime = DateTime::<Local>::from_str(datetime_str).expect("valid input");
245
246 let ltr = LocalNaiveTimeRange::from_str(s)?;
247 ltr.after_datetime(&datetime, allow_time_inside_range)
248 .ok_or_else(|| anyhow!("ambiguous: {ltr}"))
249 }
250
251 #[test]
252 fn t_after_datetime_within_first_day() -> Result<()> {
253 assert_eq!(
254 datetime_to_locals("23:00-6:00", "2024-10-23T23:30:00+02:00", true)?.to_string(),
255 "2024-10-23T23:00:00+02:00 - 2024-10-24T06:00:00+02:00"
256 );
257 assert_eq!(
258 datetime_to_locals("23:00-6:00", "2024-10-23T23:30:00+02:00", false)?.to_string(),
259 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
260 );
261 Ok(())
262 }
263
264 #[test]
265 fn t_after_datetime_within_first_day_on_start() -> Result<()> {
266 assert_eq!(
267 datetime_to_locals("23:00-6:00", "2024-10-23T23:00:00+02:00", true)?.to_string(),
268 "2024-10-23T23:00:00+02:00 - 2024-10-24T06:00:00+02:00"
269 );
270 assert_eq!(
271 datetime_to_locals("23:00-6:00", "2024-10-23T23:00:00+02:00", false)?.to_string(),
272 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
273 );
274 Ok(())
275 }
276
277 #[test]
278 fn t_after_datetime_within_first_day_before_end() -> Result<()> {
279 assert_eq!(
280 datetime_to_locals("23:00-6:00", "2024-10-24T05:59:59+02:00", true)?.to_string(),
281 "2024-10-23T23:00:00+02:00 - 2024-10-24T06:00:00+02:00"
282 );
283 assert_eq!(
284 datetime_to_locals("23:00-6:00", "2024-10-24T05:59:59+02:00", false)?.to_string(),
285 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
286 );
287 Ok(())
288 }
289
290 #[test]
291 fn t_after_datetime_within_first_day_on_end() -> Result<()> {
292 assert_eq!(
293 datetime_to_locals("23:00-6:00", "2024-10-24T06:00:00+02:00", true)?.to_string(),
294 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
295 );
296 assert_eq!(
297 datetime_to_locals("23:00-6:00", "2024-10-24T06:00:00+02:00", false)?.to_string(),
298 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
299 );
300 Ok(())
301 }
302
303 #[test]
304 fn t_after_datetime_within_second_day() -> Result<()> {
305 assert_eq!(
306 datetime_to_locals("23:00-6:00", "2024-10-24T02:00:00+02:00", true)?.to_string(),
307 "2024-10-23T23:00:00+02:00 - 2024-10-24T06:00:00+02:00"
308 );
309 assert_eq!(
310 datetime_to_locals("23:00-6:00", "2024-10-24T02:00:00+02:00", false)?.to_string(),
311 "2024-10-24T23:00:00+02:00 - 2024-10-25T06:00:00+02:00"
312 );
313 Ok(())
314 }
315
316 #[test]
317 fn t_after_datetime_on_start_boundary() -> Result<()> {
318 assert_eq!(
319 datetime_to_locals("2:00-6:00", "2024-10-23T02:00:00+02:00", true)?.to_string(),
320 "2024-10-23T02:00:00+02:00 - 2024-10-23T06:00:00+02:00"
321 );
322 assert_eq!(
323 datetime_to_locals("2:00-6:00", "2024-10-23T02:00:00+02:00", false)?.to_string(),
324 "2024-10-24T02:00:00+02:00 - 2024-10-24T06:00:00+02:00"
325 );
326 Ok(())
327 }
328
329 #[test]
330 fn t_after_datetime_starting_on_day_boundary() -> Result<()> {
331 assert_eq!(
332 datetime_to_locals("0:00-6:00", "2024-10-23T00:00:00+02:00", true)?.to_string(),
333 "2024-10-23T00:00:00+02:00 - 2024-10-23T06:00:00+02:00"
334 );
335 assert_eq!(
336 datetime_to_locals("0:00-6:00", "2024-10-23T00:00:00+02:00", false)?.to_string(),
337 "2024-10-24T00:00:00+02:00 - 2024-10-24T06:00:00+02:00"
338 );
339 Ok(())
340 }
341
342 #[test]
343 fn t_after_datetime_ending_on_day_boundary() -> Result<()> {
344 assert_eq!(
345 datetime_to_locals("6:00-0:00", "2024-10-23T00:00:00+02:00", true)?.to_string(),
346 "2024-10-23T06:00:00+02:00 - 2024-10-24T00:00:00+02:00"
347 );
348 assert_eq!(
349 datetime_to_locals("6:00-0:00", "2024-10-23T00:00:00+02:00", false)?.to_string(),
350 "2024-10-23T06:00:00+02:00 - 2024-10-24T00:00:00+02:00"
351 );
352 Ok(())
353 }
354
355 #[test]
356 fn t_after_datetime_last_on_day_boundary() -> Result<()> {
357 assert_eq!(
358 datetime_to_locals("6:00-0:00", "2024-10-23T23:59:59+02:00", true)?.to_string(),
359 "2024-10-23T06:00:00+02:00 - 2024-10-24T00:00:00+02:00"
360 );
361 assert_eq!(
362 datetime_to_locals("6:00-0:00", "2024-10-23T23:59:59+02:00", false)?.to_string(),
363 "2024-10-24T06:00:00+02:00 - 2024-10-25T00:00:00+02:00"
364 );
365 Ok(())
366 }
367
368 #[test]
370 fn t_after_datetime_empty() -> Result<()> {
371 assert_eq!(
372 datetime_to_locals("6:00-6:00", "2024-10-23T00:00:00+02:00", true)?.to_string(),
373 "2024-10-23T06:00:00+02:00 - 2024-10-23T06:00:00+02:00"
374 );
375 assert_eq!(
376 datetime_to_locals("6:00-6:00", "2024-10-23T00:00:00+02:00", false)?.to_string(),
377 "2024-10-23T06:00:00+02:00 - 2024-10-23T06:00:00+02:00"
378 );
379 Ok(())
380 }
381}