financial_recurrence/
lib.rs

1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3#![warn(clippy::pedantic)]
4#![allow(clippy::unreadable_literal)]
5#![doc = include_str!("../README.md")]
6
7/// Occurrences of this rule, and iterators to handle them.
8pub mod occurrences;
9
10use bitflags::bitflags;
11use chrono::NaiveDate;
12use getset::{Getters, Setters};
13use occurrences::Iter;
14
15/// A recurrence rule
16#[derive(Getters, Setters, Clone, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[getset(get = "pub")]
19pub struct RecurrenceRule {
20    /// This rule is not valid before the specified [`NaiveDate`] (inclusive)
21    not_before: NaiveDate,
22
23    /// This rule is not valid after the specified [`NaiveDate`]
24    not_after: Option<NaiveDate>,
25
26    /// The maximum number of occurrences for this rule, or unlimited if [`None`].
27    #[getset(set = "pub")]
28    max_occurrences: Option<u64>,
29
30    /// At what frequency should this rule reoccur?
31    #[getset(set = "pub")]
32    frequency: Frequency,
33
34    /// A bitflag day filter, xMTWTFSS, 01111111 means this can be paid any day.
35    #[getset(set = "pub")]
36    day_filter: DayFilter,
37
38    /// If the bitflag day filter cannot be met, should it be resolved into the future or the past?
39    #[getset(set = "pub")]
40    resolve: ResolveDirection,
41}
42
43impl RecurrenceRule {
44    /// Create a new recurrence rule.
45    #[must_use]
46    pub fn new(frequency: Frequency, not_before: NaiveDate) -> Self {
47        Self {
48            not_before,
49            not_after: None,
50            max_occurrences: None,
51            frequency,
52            day_filter: DayFilter::EVERYDAY,
53            resolve: ResolveDirection::IntoFuture,
54        }
55    }
56
57    /// Set the date at which this rule is not valid before. Will return false
58    /// if it wasn't able to set due to the provided date being after the
59    /// `not_after` date.
60    #[must_use = "If you do not need to validate the checking, use `set_not_before_unchecked"]
61    pub fn set_not_before(&mut self, not_before: NaiveDate) -> bool {
62        if let Some(not_after) = &self.not_after {
63            if &not_before > not_after {
64                return false;
65            }
66        }
67        self.set_not_before_unchecked(not_before);
68        true
69    }
70
71    /// Set the date at which this rule is not valid before.
72    ///
73    /// Preferably, you should use [`Self::set_not_before`].
74    pub fn set_not_before_unchecked(&mut self, not_before: NaiveDate) {
75        self.not_before = not_before;
76    }
77
78    /// Set the date at which this rule is not valid before. Will return false
79    /// if it wasn't able to set due to the provided date being after the
80    /// `not_after` date.
81    #[must_use = "If you do not need to validate the checking, use `set_not_after_unchecked"]
82    pub fn set_not_after(&mut self, not_after: Option<NaiveDate>) -> bool {
83        if let Some(not_after_dt) = &not_after {
84            if not_after_dt < &self.not_before {
85                return false;
86            }
87        }
88        self.set_not_after_unchecked(not_after);
89        true
90    }
91
92    /// Set the date at which this rule is not valid after.
93    ///
94    /// Preferably, you should use [`Self::set_not_after`].
95    pub fn set_not_after_unchecked(&mut self, not_after: Option<NaiveDate>) {
96        self.not_after = not_after;
97    }
98
99    /// Create an iterator for all occurrences, starting at the provided `start_point` (exclusive).
100    #[must_use]
101    pub fn iter_after(&self, start_point: &NaiveDate) -> Iter {
102        Iter {
103            currently_at: *start_point,
104            index: 0,
105            rule: self,
106        }
107    }
108
109    /// Create an iterator for all occurrences, starting at [`Self::not_before`] (inclusive).
110    ///
111    /// ## Panics
112    ///
113    /// This will panic if [`Self::not_before`] is the beginning of time.
114    #[must_use]
115    pub fn iter(&self) -> Iter {
116        self.iter_after(
117            &self
118                .not_before()
119                .checked_sub_days(chrono::Days::new(1))
120                .unwrap(),
121        )
122    }
123}
124
125impl<'a> IntoIterator for &'a RecurrenceRule {
126    type Item = occurrences::Occurrence;
127    type IntoIter = occurrences::Iter<'a>;
128
129    fn into_iter(self) -> Self::IntoIter {
130        self.iter()
131    }
132}
133
134/// The frequency of a recurring rule
135#[derive(Debug, Clone, Copy)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub enum Frequency {
138    /// Reoccur weekly
139    Weekly {
140        /// Which days should this be taken on
141        days: DayFilter,
142    },
143    /// Reoccur monthly
144    Monthly {
145        /// The date of the month (1-indexed)
146        date: u8,
147    },
148    /// Reoccur yearly
149    Yearly {
150        /// The date of the month (1-indexed)
151        date: u8,
152        /// The month of the year (1-indexed)
153        month: u8,
154    },
155}
156
157/// If the desired day cannot be resolved, should we find the next day into the
158/// future that satisfies the requirement, or the day into the past.
159#[derive(Debug, Clone, Copy)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161pub enum ResolveDirection {
162    /// Resolve by looking forward into the future for the next valid day
163    IntoFuture,
164    /// Resolve by looking back into the past for the next valid day
165    IntoPast,
166}
167
168bitflags! {
169    /// Filter by days of the week
170    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
171    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172    pub struct DayFilter: u8 {
173        /// Monday
174        const MONDAY = 0b1000000;
175        /// Tuesday
176        const TUESDAY = 0b0100000;
177        /// Wednesday
178        const WEDNESDAY = 0b0010000;
179        /// Thursday
180        const THURSDAY = 0b0001000;
181        /// Friday
182        const FRIDAY = 0b0000100;
183        /// Saturday
184        const SATURDAY = 0b0000010;
185        /// Sunday
186        const SUNDAY = 0b0000001;
187
188        /// Every day of the week. Equivalent to `ANYDAY`.
189        const EVERYDAY = Self::MONDAY.bits()
190                        | Self::TUESDAY.bits()
191                        | Self::WEDNESDAY.bits()
192                        | Self::THURSDAY.bits()
193                        | Self::FRIDAY.bits()
194                        | Self::SATURDAY.bits()
195                        | Self::SUNDAY.bits();
196        /// Any day of the week. Equivalent to `EVERYDAY`.
197        const ANYDAY = Self::EVERYDAY.bits();
198        /// Only weekdays
199        const WEEKDAYS = Self::MONDAY.bits()
200                        | Self::TUESDAY.bits()
201                        | Self::WEDNESDAY.bits()
202                        | Self::THURSDAY.bits()
203                        | Self::FRIDAY.bits();
204        /// Only weekends
205        const WEEKENDS = Self::SATURDAY.bits()
206                        | Self::SUNDAY.bits();
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_not_before_checking() {
216        let mut rule = RecurrenceRule::new(
217            Frequency::Monthly { date: 1 },
218            NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
219        );
220        rule.set_not_after_unchecked(Some(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()));
221        let allowed = rule.set_not_before(NaiveDate::from_ymd_opt(2001, 1, 1).unwrap());
222        assert!(!allowed);
223        let allowed = rule.set_not_before(NaiveDate::from_ymd_opt(1999, 1, 1).unwrap());
224        assert!(allowed);
225    }
226
227    #[test]
228    fn test_not_after_checking() {
229        let mut rule = RecurrenceRule::new(
230            Frequency::Monthly { date: 1 },
231            NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
232        );
233        let allowed = rule.set_not_after(Some(NaiveDate::from_ymd_opt(1999, 1, 1).unwrap()));
234        assert!(!allowed);
235        let allowed = rule.set_not_after(Some(NaiveDate::from_ymd_opt(2001, 1, 1).unwrap()));
236        assert!(allowed);
237    }
238}