recurring/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3#![warn(missing_docs)]
4#![warn(clippy::pedantic)]
5#![allow(clippy::must_use_candidate, clippy::struct_field_names)]
6
7extern crate alloc;
8
9mod error;
10mod event;
11pub mod pattern;
12mod range;
13pub mod series;
14
15use core::ops::{Bound, Range, RangeBounds, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive};
16pub use error::Error;
17pub use event::Event;
18use jiff::civil::{Date, DateTime, time};
19use jiff::{ToSpan, Zoned};
20use pattern::Combined;
21pub use range::DateTimeRange;
22#[doc(inline)]
23pub use series::Series;
24
25mod private {
26    pub trait Sealed {}
27}
28
29/// A trait for recurrence patterns.
30///
31/// Values implementing `Pattern` are passed to [`Series::new`][Series::new] or [`Series::try_new`]
32/// to build a new series of recurring events.
33///
34/// Since values implementing this trait must uphold some invariants to ensure correctness it is
35/// sealed to prevent implementing it outside of this crate.
36///
37/// There is usually no need to interact with this trait directly. Use the functionality provided
38/// by [`Series`] instead because it is more convenient.
39///
40/// The [`pattern`] module contains implementations of various recurrence patterns.
41pub trait Pattern: private::Sealed + Clone {
42    /// Find the next `DateTime` after `instant` within a range.
43    ///
44    /// This must always returns a datetime that is strictly larger than `instant` or `None` if
45    /// the next event would be greater or equal to the range's end. If `instant` happens before
46    /// the range's start, this must return the first event within the range.
47    fn next_after(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime>;
48
49    /// Find the previous `DateTime` before `instant` within a range.
50    ///
51    /// This must always returns a datetime that is strictly smaller than `instant` or `None` if
52    /// the previous event would be less than the range's start. If `instant` happens after
53    /// the range's end, this must return the last event within the range.
54    fn previous_before(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime>;
55
56    /// Find a `DateTime` closest to `instant` within a range.
57    ///
58    /// The returned datetime may happen before, after and exactly at `instant`. This must only
59    /// return `None` if there is no event within the range.
60    fn closest_to(&self, instant: DateTime, range: DateTimeRange) -> Option<DateTime>;
61}
62
63/// A trait for combining values implementing [`Pattern`] into more complex recurrence patterns.
64///
65/// This trait has a blanket implementation for any type implementing `Pattern`.
66///
67/// # Example
68///
69/// ```
70/// use recurring::{Combine, pattern::cron};
71///
72/// let daily_at_noon = cron().hour(12).minute(0).second(0);
73/// let daily_at_midnight = cron().hour(0).minute(0).second(0);
74/// let first_of_month_at_six = cron().day(1).hour(6).minute(0).second(0);
75///
76/// let combined = daily_at_noon
77///     .and(daily_at_midnight)
78///     .and(first_of_month_at_six);
79/// ```
80pub trait Combine: Pattern + Sized {
81    /// Combine `Self` with another `Pattern`.
82    ///
83    /// This allows building more complex recurrence patterns.
84    ///
85    /// See the documentation of the [`Combine`] trait for usage examples.
86    #[must_use]
87    fn and<P: Pattern>(self, other: P) -> Combined<Self, P> {
88        Combined::new(self, other)
89    }
90}
91
92impl<T: Pattern> Combine for T {}
93
94/// A trait for converting values representing points in time into a [`Series`].
95pub trait ToSeries {
96    /// Converts a value to a [`Series`] with the given recurrence [`Pattern`].
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the value cannot be converted into a valid `Series`.
101    fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error>;
102}
103
104impl ToSeries for Event {
105    /// Converts an `Event` to a `Series` with the given recurrence [`Pattern`].
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the event duration cannot be represented as a [`Span`][jiff::Span] or
110    /// if the events' `start` is `DateTime::MAX`.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use jiff::civil::date;
116    /// use recurring::{Event, ToSeries, pattern::hourly};
117    ///
118    /// let date = date(2025, 1, 1);
119    /// let start = date.at(0, 0, 0, 0);
120    /// let end = date.at(0, 30, 0, 0);
121    ///
122    /// let event = Event::new(start, end);
123    /// let series = event.to_series(hourly(2))?;
124    ///
125    /// let mut events = series.iter();
126    ///
127    /// assert_eq!(events.next(), Some(Event::new(date.at(0, 0, 0, 0), date.at(0, 30, 0, 0))));
128    /// assert_eq!(events.next(), Some(Event::new(date.at(2, 0, 0, 0), date.at(2, 30, 0, 0))));
129    /// # Ok::<(), Box<dyn core::error::Error>>(())
130    /// ```
131    fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error> {
132        Series::builder(self.start().., pattern)
133            .event_duration(self.duration())
134            .build()
135    }
136}
137
138impl ToSeries for DateTime {
139    /// Converts a `DateTime` to a `Series` with the given recurrence [`Pattern`].
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the datetime is `DateTime::MAX`.
144    ///
145    /// # Example
146    ///
147    /// ```
148    /// use jiff::civil::datetime;
149    /// use recurring::{Event, ToSeries, pattern::hourly};
150    ///
151    /// let series = datetime(2025, 1, 1, 0, 0, 0, 0).to_series(hourly(2))?;
152    ///
153    /// let mut events = series.iter();
154    ///
155    /// assert_eq!(events.next(), Some(Event::at(datetime(2025, 1, 1, 0, 0, 0, 0))));
156    /// assert_eq!(events.next(), Some(Event::at(datetime(2025, 1, 1, 2, 0, 0, 0))));
157    /// # Ok::<(), Box<dyn core::error::Error>>(())
158    /// ```
159    fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error> {
160        Series::try_new(*self.., pattern)
161    }
162}
163
164impl ToSeries for Date {
165    /// Converts a `Date` to a `Series` with the given recurrence [`Pattern`].
166    ///
167    /// The resulting series always starts at midnight on the date `to_series` is called on.
168    ///
169    /// # Errors
170    ///
171    /// This method does not fail for `Date`.
172    ///
173    /// # Example
174    ///
175    /// ```
176    /// use jiff::civil::date;
177    /// use recurring::{Event, ToSeries, pattern::hourly};
178    ///
179    /// let series = date(2025, 1, 1).to_series(hourly(2))?;
180    ///
181    /// let mut events = series.iter();
182    ///
183    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(0, 0, 0, 0))));
184    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(2, 0, 0, 0))));
185    /// # Ok::<(), Box<dyn core::error::Error>>(())
186    /// ```
187    fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error> {
188        self.to_datetime(time(0, 0, 0, 0)).to_series(pattern)
189    }
190}
191
192impl ToSeries for Zoned {
193    /// Converts a `Date` to a `Series` with the given recurrence [`Pattern`].
194    ///
195    /// The resulting series always starts at midnight on the date `to_series` is called on.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the `Zoned`'s datetime is `DateTime::MAX`.
200    ///
201    /// # Example
202    ///
203    /// ```
204    /// use jiff::{Zoned, civil::date};
205    /// use recurring::{Event, ToSeries, pattern::hourly};
206    ///
207    /// let zoned: Zoned = "2025-01-01 12:22[Europe/Berlin]".parse()?;
208    ///
209    /// let series = zoned.to_series(hourly(2))?;
210    ///
211    /// let mut events = series.iter();
212    ///
213    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(12, 22, 0, 0))));
214    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(14, 22, 0, 0))));
215    /// # Ok::<(), Box<dyn core::error::Error>>(())
216    /// ```
217    fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error> {
218        self.datetime().to_series(pattern)
219    }
220}
221
222macro_rules! impl_range_to_series {
223    ($($(#[doc = $doc:expr])+ $ty:ty,)+) => {
224        $(
225            impl ToSeries for $ty {
226                /// Converts a `
227                #[doc = stringify!($ty)]
228                /// ` to a `Series` with the given recurrence [`Pattern`].
229                ///
230                /// # Example
231                ///
232                $(#[doc = $doc])*
233                fn to_series<P: Pattern>(&self, pattern: P) -> Result<Series<P>, Error> {
234                    Series::try_new(self.clone(), pattern)
235                }
236            }
237        )*
238    };
239}
240
241impl_range_to_series!(
242    /// ```
243    /// use jiff::civil::date;
244    /// use recurring::{Event, ToSeries, pattern::hourly};
245    ///
246    /// let start = date(2025, 1, 1).at(0, 0, 0, 0);
247    /// let end = date(2025, 1, 1).at(4, 0, 0, 0);
248    /// let series = (start..end).to_series(hourly(2))?;
249    ///
250    /// let mut events = series.iter();
251    ///
252    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(0, 0, 0, 0))));
253    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(2, 0, 0, 0))));
254    /// assert_eq!(events.next(), None);
255    /// # Ok::<(), Box<dyn core::error::Error>>(())
256    /// ```
257    Range<DateTime>,
258    /// ```
259    /// use recurring::{Event, ToSeries, pattern::hourly};
260    /// use jiff::civil::date;
261    ///
262    /// let start = date(2025, 1, 1).at(0, 0, 0, 0);
263    /// let series = (start..).to_series(hourly(2))?;
264    ///
265    /// let mut events = series.iter();
266    ///
267    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(0, 0, 0, 0))));
268    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(2, 0, 0, 0))));
269    /// # Ok::<(), Box<dyn core::error::Error>>(())
270    /// ```
271    RangeFrom<DateTime>,
272    /// ```
273    /// use recurring::{Event, ToSeries, pattern::hourly};
274    /// use jiff::civil::date;
275    ///
276    /// let start = date(2025, 1, 1).at(0, 0, 0, 0);
277    /// let end = date(2025, 1, 1).at(4, 0, 0, 0);
278    /// let series = (start..=end).to_series(hourly(2))?;
279    ///
280    /// let mut events = series.iter();
281    ///
282    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(0, 0, 0, 0))));
283    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(2, 0, 0, 0))));
284    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(4, 0, 0, 0))));
285    /// assert_eq!(events.next(), None);
286    /// # Ok::<(), Box<dyn core::error::Error>>(())
287    /// ```
288    RangeInclusive<DateTime>,
289    /// ```
290    /// use recurring::{Event, ToSeries, pattern::hourly};
291    /// use jiff::civil::{DateTime, date};
292    ///
293    /// let end = DateTime::MAX;
294    /// let series = (..end).to_series(hourly(2))?;
295    ///
296    /// let mut events = series.iter();
297    ///
298    /// assert_eq!(events.next(), Some(Event::at(date(-9999, 1, 1).at(0, 0, 0, 0))));
299    /// assert_eq!(events.next(), Some(Event::at(date(-9999, 1, 1).at(2, 0, 0, 0))));
300    /// # Ok::<(), Box<dyn core::error::Error>>(())
301    /// ```
302    RangeTo<DateTime>,
303    /// ```
304    /// use recurring::{Event, ToSeries, pattern::hourly};
305    /// use jiff::civil::date;
306    ///
307    /// let end = date(2025, 1, 1).at(4, 0, 0, 0);
308    /// let series = (..=end).to_series(hourly(2))?;
309    ///
310    /// let mut events = series.iter();
311    ///
312    /// assert_eq!(events.next(), Some(Event::at(date(-9999, 1, 1).at(0, 0, 0, 0))));
313    /// assert_eq!(events.next(), Some(Event::at(date(-9999, 1, 1).at(2, 0, 0, 0))));
314    /// # Ok::<(), Box<dyn core::error::Error>>(())
315    /// ```
316    RangeToInclusive<DateTime>,
317    /// ```
318    /// use recurring::{Event, ToSeries, pattern::hourly};
319    /// use jiff::civil::date;
320    /// use core::ops::Bound;
321    ///
322    /// let start = date(2025, 1, 1).at(0, 0, 0, 0);
323    /// let end = date(2025, 1, 1).at(4, 0, 0, 0);
324    /// let series = (Bound::Included(start), Bound::Excluded(end)).to_series(hourly(2))?;
325    ///
326    /// let mut events = series.iter();
327    ///
328    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(0, 0, 0, 0))));
329    /// assert_eq!(events.next(), Some(Event::at(date(2025, 1, 1).at(2, 0, 0, 0))));
330    /// assert_eq!(events.next(), None);
331    /// # Ok::<(), Box<dyn core::error::Error>>(())
332    /// ```
333    (Bound<DateTime>, Bound<DateTime>),
334);
335
336/// @TODO(mohmann): replace with `core::ops::IntoBounds` once it's stable.
337trait IntoBounds<T> {
338    fn into_bounds(self) -> (Bound<T>, Bound<T>);
339}
340
341impl<B: RangeBounds<T>, T: Clone> IntoBounds<T> for B {
342    fn into_bounds(self) -> (Bound<T>, Bound<T>) {
343        (self.start_bound().cloned(), self.end_bound().cloned())
344    }
345}
346
347// Tries to simplify arbitrary range bounds into a `DateTimeRange`.
348fn try_simplify_range<B: RangeBounds<DateTime>>(bounds: B) -> Result<DateTimeRange, Error> {
349    let start = match bounds.start_bound() {
350        Bound::Unbounded => DateTime::MIN,
351        Bound::Included(start) => *start,
352        Bound::Excluded(start) => start.checked_add(1.nanosecond())?,
353    };
354
355    let end = match bounds.end_bound() {
356        Bound::Unbounded => DateTime::MAX,
357        Bound::Included(end) => end.checked_add(1.nanosecond())?,
358        Bound::Excluded(end) => *end,
359    };
360
361    Ok(DateTimeRange::new(start, end))
362}