financial_recurrence/
lib.rs1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3#![warn(clippy::pedantic)]
4#![allow(clippy::unreadable_literal)]
5#![doc = include_str!("../README.md")]
6
7pub mod occurrences;
9
10use bitflags::bitflags;
11use chrono::NaiveDate;
12use getset::{Getters, Setters};
13use occurrences::Iter;
14
15#[derive(Getters, Setters, Clone, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[getset(get = "pub")]
19pub struct RecurrenceRule {
20 not_before: NaiveDate,
22
23 not_after: Option<NaiveDate>,
25
26 #[getset(set = "pub")]
28 max_occurrences: Option<u64>,
29
30 #[getset(set = "pub")]
32 frequency: Frequency,
33
34 #[getset(set = "pub")]
36 day_filter: DayFilter,
37
38 #[getset(set = "pub")]
40 resolve: ResolveDirection,
41}
42
43impl RecurrenceRule {
44 #[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 #[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 ¬_before > not_after {
64 return false;
65 }
66 }
67 self.set_not_before_unchecked(not_before);
68 true
69 }
70
71 pub fn set_not_before_unchecked(&mut self, not_before: NaiveDate) {
75 self.not_before = not_before;
76 }
77
78 #[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) = ¬_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 pub fn set_not_after_unchecked(&mut self, not_after: Option<NaiveDate>) {
96 self.not_after = not_after;
97 }
98
99 #[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 #[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#[derive(Debug, Clone, Copy)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub enum Frequency {
138 Weekly {
140 days: DayFilter,
142 },
143 Monthly {
145 date: u8,
147 },
148 Yearly {
150 date: u8,
152 month: u8,
154 },
155}
156
157#[derive(Debug, Clone, Copy)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161pub enum ResolveDirection {
162 IntoFuture,
164 IntoPast,
166}
167
168bitflags! {
169 #[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 const MONDAY = 0b1000000;
175 const TUESDAY = 0b0100000;
177 const WEDNESDAY = 0b0010000;
179 const THURSDAY = 0b0001000;
181 const FRIDAY = 0b0000100;
183 const SATURDAY = 0b0000010;
185 const SUNDAY = 0b0000001;
187
188 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 const ANYDAY = Self::EVERYDAY.bits();
198 const WEEKDAYS = Self::MONDAY.bits()
200 | Self::TUESDAY.bits()
201 | Self::WEDNESDAY.bits()
202 | Self::THURSDAY.bits()
203 | Self::FRIDAY.bits();
204 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}