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}