evobench_tools/date_and_time/
time_ranges.rs

1//! Using serde::date_and_time, but putting this code into another
2//! module to keep the scope of the serde::* namespace narrow.
3
4use 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    /// Returns None if there is ambiguity (due to daylight savings
72    /// time switches, or perhaps leap seconds?). *Note* that this
73    /// literally just adds the given date as the date of the start
74    /// time, then if necessary increments the date if the end time <
75    /// start time (range crosses a day boundary). Taking the date
76    /// from the current time and then passing it to this method means
77    /// that the current time can be past the range, and it also means
78    /// that even though the current time might be within the range,
79    /// the result is in the future (i.e. if the start time of the
80    /// range is before a day boundary, this method still resolves it
81    /// to the given date, resulting in a time that is in the
82    /// future). You probably want to use `after_datetime()` instead.
83    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    /// Returns None if there is ambiguity (due to daylight savings
99    /// time switches, or perhaps leap seconds?) (or if given a
100    /// `datetime` for which no day can be added (max date?)). If
101    /// `allow_time_inside_range` is true, picks the start date so
102    /// that the resulting range contains `ndt` if possible, otherwise
103    /// the resulting range starts >= `ndt`.  XX new desc: the time
104    /// period using the `self` times, closest to `datetime`, around
105    /// it if allowed or closest after it. Does not need to carry the
106    /// same date!
107    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        // Try with on the same day
128        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                // Take the next day
146                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                // `dtr_today` is in the future. But check the
156                // day before that, it might be closer.
157                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
196// Docs for Eq say should not impl by hand, but, ??. DateTime<Tz>
197// *does* impl it, so.
198impl<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    // whole day? ah no, empty
369    #[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}