recurring/pattern/
daily.rs

1use crate::pattern::Interval;
2use crate::{DateTimeRange, Error, Pattern, private};
3use jiff::{
4    ToSpan,
5    civil::{DateTime, Time},
6};
7
8/// A recurrence pattern for daily events which may also include fixed times of the day.
9///
10/// # Example
11///
12/// ```
13/// use recurring::pattern::Daily;
14/// use jiff::civil::time;
15///
16/// let every_two_days_at_twelve = Daily::new(1).at(time(12, 0, 0, 0));
17/// ```
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Daily {
20    interval: Interval,
21    at: Option<Time>,
22}
23
24impl Daily {
25    /// Creates a new `Daily` from an interval of days.
26    ///
27    /// The fallible version of this method is [`Daily::try_new`].
28    ///
29    /// # Panics
30    ///
31    /// Panics if `interval` is negative or zero.
32    ///
33    /// # Example
34    ///
35    /// ```
36    /// use recurring::pattern::Daily;
37    ///
38    /// let every_two_days = Daily::new(2);
39    /// ```
40    pub fn new<I: ToSpan>(interval: I) -> Daily {
41        Daily {
42            interval: Interval::new(interval.days()),
43            at: None,
44        }
45    }
46
47    /// Creates a new `Daily` from an interval of days.
48    ///
49    /// The panicking version of this method is [`Daily::new`].
50    ///
51    /// # Errors
52    ///
53    /// Returns an `Error` if `interval` is negative or zero.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// use recurring::pattern::Daily;
59    ///
60    /// assert!(Daily::try_new(1).is_ok());
61    /// assert!(Daily::try_new(0).is_err());
62    /// assert!(Daily::try_new(-1).is_err());
63    /// ```
64    pub fn try_new<I: ToSpan>(interval: I) -> Result<Daily, Error> {
65        Ok(Daily {
66            interval: Interval::try_new(interval.days())?,
67            at: None,
68        })
69    }
70
71    /// Sets the exact time of day for the daily recurrence.
72    ///
73    /// # Example
74    ///
75    /// ```
76    /// use jiff::civil::time;
77    /// use recurring::pattern::daily;
78    ///
79    /// let every_day_at_twelve = daily(1).at(time(12, 0, 0, 0));
80    ///
81    /// let every_day_at_midnight_and_twelve = every_day_at_twelve.at(time(0, 0, 0, 0));
82    /// ```
83    #[must_use]
84    pub fn at<T: Into<Time>>(mut self, time: T) -> Daily {
85        self.at = Some(time.into());
86        self
87    }
88
89    fn range_adjusted<F>(&self, f: F, instant: DateTime, range: DateTimeRange) -> Option<DateTime>
90    where
91        F: FnOnce(&Interval, DateTime, DateTimeRange) -> Option<DateTime>,
92    {
93        let Some(time) = self.at else {
94            return f(&self.interval, instant, range);
95        };
96
97        let start = if range.start.time() <= time {
98            range.start
99        } else {
100            self.interval.next_after(range.start, range)?
101        };
102
103        let start = start.with().time(time).build().ok()?;
104        let range = DateTimeRange::from(start..range.end);
105
106        f(&self.interval, instant, range)
107    }
108}
109
110impl Pattern for Daily {
111    fn next_after(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime> {
112        self.range_adjusted(Interval::next_after, instant, range)
113    }
114
115    fn previous_before(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime> {
116        self.range_adjusted(Interval::previous_before, instant, range)
117    }
118
119    fn closest_to(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime> {
120        self.range_adjusted(Interval::closest_to, instant, range)
121    }
122}
123
124impl private::Sealed for Daily {}