Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/interval/recurrence.rs

10.8 KiB, 55 runs

created by r1870400018:6375, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Recurrence rules and the patterns built from them.
2//!
3//! A rule fixes a frequency and an interval, then narrows the dates that
4//! generates with by_weekday, by_month_day and the rest. A pattern pairs a
5//! rule with a start time and a set of dates to skip.
6//!
7//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
8//! Anthropic Claude
9
10use crate::{
11 calendar::CalendarDate,
12 constant::{DayOfWeek, MonthOfYear},
13 time::CalClock,
14};
15
16use oxedyne_fe2o3_core::prelude::*;
17
18use std::collections::HashSet;
19
20#[derive(Clone, Debug, PartialEq)]
21pub enum Frequency {
22 Daily,
23 Weekly,
24 Monthly,
25 Yearly,
26 Hourly,
27 Minutely,
28 Secondly,
29}
30
31#[derive(Clone, Debug)]
32pub struct RecurrenceRule {
33 frequency: Frequency,
34 interval: u32, // 2 means every other one
35 until: Option<CalendarDate>,
36 count: Option<u32>,
37 // Filters applied to the dates the frequency generates.
38 by_weekday: Option<HashSet<DayOfWeek>>,
39 by_month_day: Option<HashSet<u8>>, // 1-31
40 by_month: Option<HashSet<MonthOfYear>>,
41 by_hour: Option<HashSet<u8>>, // 0-23
42 by_minute: Option<HashSet<u8>>, // 0-59
43 by_second: Option<HashSet<u8>>, // 0-59
44}
45
46impl RecurrenceRule {
47 pub fn new(frequency: Frequency) -> Self {
48 Self {
49 frequency,
50 interval: 1,
51 until: None,
52 count: None,
53 by_weekday: None,
54 by_month_day: None,
55 by_month: None,
56 by_hour: None,
57 by_minute: None,
58 by_second: None,
59 }
60 }
61
62 pub fn interval(mut self, interval: u32) -> Self {
63 self.interval = interval.max(1);
64 self
65 }
66
67 pub fn until(mut self, until: CalendarDate) -> Self {
68 self.until = Some(until);
69 self
70 }
71
72 pub fn count(mut self, count: u32) -> Self {
73 self.count = Some(count);
74 self
75 }
76
77 pub fn by_weekday(mut self, weekdays: HashSet<DayOfWeek>) -> Self {
78 self.by_weekday = Some(weekdays);
79 self
80 }
81
82 pub fn by_month_day(mut self, month_days: HashSet<u8>) -> Self {
83 self.by_month_day = Some(month_days);
84 self
85 }
86
87 pub fn by_month(mut self, months: HashSet<MonthOfYear>) -> Self {
88 self.by_month = Some(months);
89 self
90 }
91
92 pub fn by_hour(mut self, hours: HashSet<u8>) -> Self {
93 self.by_hour = Some(hours);
94 self
95 }
96
97 pub fn by_minute(mut self, minutes: HashSet<u8>) -> Self {
98 self.by_minute = Some(minutes);
99 self
100 }
101
102 pub fn by_second(mut self, seconds: HashSet<u8>) -> Self {
103 self.by_second = Some(seconds);
104 self
105 }
106
107 // ========================================================================
108 // Convenience Constructors
109 // ========================================================================
110
111 pub fn daily() -> Self {
112 Self::new(Frequency::Daily)
113 }
114
115 pub fn weekly() -> Self {
116 Self::new(Frequency::Weekly)
117 }
118
119 pub fn monthly() -> Self {
120 Self::new(Frequency::Monthly)
121 }
122
123 pub fn yearly() -> Self {
124 Self::new(Frequency::Yearly)
125 }
126
127 pub fn business_days() -> Self {
128 let mut weekdays = HashSet::new();
129 weekdays.insert(DayOfWeek::Monday);
130 weekdays.insert(DayOfWeek::Tuesday);
131 weekdays.insert(DayOfWeek::Wednesday);
132 weekdays.insert(DayOfWeek::Thursday);
133 weekdays.insert(DayOfWeek::Friday);
134
135 Self::new(Frequency::Weekly).by_weekday(weekdays)
136 }
137
138 pub fn weekends() -> Self {
139 let mut weekdays = HashSet::new();
140 weekdays.insert(DayOfWeek::Saturday);
141 weekdays.insert(DayOfWeek::Sunday);
142
143 Self::new(Frequency::Weekly).by_weekday(weekdays)
144 }
145
146 pub fn first_of_month() -> Self {
147 let mut month_days = HashSet::new();
148 month_days.insert(1);
149
150 Self::new(Frequency::Monthly).by_month_day(month_days)
151 }
152
153 pub fn last_of_month() -> Self {
154 // This requires special handling since month lengths vary
155 Self::new(Frequency::Monthly)
156 }
157
158 // ========================================================================
159 // Pattern Generation
160 // ========================================================================
161
162 /// Stops at whichever of the count, the until date and max_occurrences
163 /// comes first, and gives up after ten thousand candidates in any case.
164 pub fn generate_occurrences(&self, start: &CalClock, max_occurrences: usize) -> Outcome<Vec<CalClock>> {
165 let mut occurrences = Vec::new();
166 let mut current = start.clone();
167 let mut count = 0;
168
169 while occurrences.len() < max_occurrences {
170 // Check if we've reached the count limit
171 if let Some(max_count) = self.count {
172 if count >= max_count {
173 break;
174 }
175 }
176
177 // Check if we've reached the until date
178 if let Some(until_date) = &self.until {
179 if current.date() > until_date {
180 break;
181 }
182 }
183
184 // Check if this occurrence matches the pattern
185 if res!(self.matches(&current)) {
186 occurrences.push(current.clone());
187 }
188
189 // Move to the next potential occurrence
190 current = res!(self.advance(&current));
191 count += 1;
192
193 // Safety check to prevent infinite loops
194 if count > 10000 {
195 break;
196 }
197 }
198
199 Ok(occurrences)
200 }
201
202 pub fn matches(&self, datetime: &CalClock) -> Outcome<bool> {
203 // Check weekday constraint
204 if let Some(ref weekdays) = self.by_weekday {
205 if !weekdays.contains(&datetime.day_of_week()) {
206 return Ok(false);
207 }
208 }
209
210 // Check month day constraint
211 if let Some(ref month_days) = self.by_month_day {
212 if !month_days.contains(&datetime.day()) {
213 return Ok(false);
214 }
215 }
216
217 // Check month constraint
218 if let Some(ref months) = self.by_month {
219 if !months.contains(&datetime.month_of_year()) {
220 return Ok(false);
221 }
222 }
223
224 // Check hour constraint
225 if let Some(ref hours) = self.by_hour {
226 if !hours.contains(&datetime.hour()) {
227 return Ok(false);
228 }
229 }
230
231 // Check minute constraint
232 if let Some(ref minutes) = self.by_minute {
233 if !minutes.contains(&datetime.minute()) {
234 return Ok(false);
235 }
236 }
237
238 // Check second constraint
239 if let Some(ref seconds) = self.by_second {
240 if !seconds.contains(&datetime.second()) {
241 return Ok(false);
242 }
243 }
244
245 Ok(true)
246 }
247
248 fn advance(&self, current: &CalClock) -> Outcome<CalClock> {
249 match self.frequency {
250 Frequency::Secondly => current.add_seconds(self.interval as i32),
251 Frequency::Minutely => current.add_minutes(self.interval as i32),
252 Frequency::Hourly => current.add_hours(self.interval as i32),
253 Frequency::Daily => current.add_days(self.interval as i32),
254 Frequency::Weekly => current.add_weeks(self.interval as i32),
255 Frequency::Monthly => current.add_months(self.interval as i32),
256 Frequency::Yearly => current.add_years(self.interval as i32),
257 }
258 }
259}
260
261#[derive(Clone, Debug)]
262pub struct RecurrencePattern {
263 start: CalClock,
264 rule: RecurrenceRule,
265 exceptions: HashSet<CalendarDate>, // dates to skip
266}
267
268impl RecurrencePattern {
269 pub fn new(start: CalClock, rule: RecurrenceRule) -> Self {
270 Self {
271 start,
272 rule,
273 exceptions: HashSet::new(),
274 }
275 }
276
277 pub fn add_exception(&mut self, date: CalendarDate) {
278 self.exceptions.insert(date);
279 }
280
281 pub fn remove_exception(&mut self, date: &CalendarDate) {
282 self.exceptions.remove(date);
283 }
284
285 pub fn exceptions(&self) -> &HashSet<CalendarDate> {
286 &self.exceptions
287 }
288
289 pub fn occurrences(&self, max_occurrences: usize) -> Outcome<Vec<CalClock>> {
290 let mut all_occurrences = res!(self.rule.generate_occurrences(&self.start, max_occurrences * 2));
291
292 // Filter out exceptions
293 all_occurrences.retain(|occurrence| {
294 !self.exceptions.contains(occurrence.date())
295 });
296
297 // Truncate to requested number
298 all_occurrences.truncate(max_occurrences);
299
300 Ok(all_occurrences)
301 }
302
303 pub fn occurrences_in_range(&self, start_date: &CalendarDate, end_date: &CalendarDate) -> Outcome<Vec<CalClock>> {
304 let max_occurrences = 1000; // Safety limit
305 let all_occurrences = res!(self.occurrences(max_occurrences));
306
307 let filtered: Vec<CalClock> = all_occurrences.into_iter()
308 .filter(|occurrence| {
309 occurrence.date() >= start_date && occurrence.date() <= end_date
310 })
311 .collect();
312
313 Ok(filtered)
314 }
315
316 pub fn next_occurrence_after(&self, after: &CalClock) -> Outcome<Option<CalClock>> {
317 let max_occurrences = 100; // Reasonable limit for searching
318 let occurrences = res!(self.rule.generate_occurrences(after, max_occurrences));
319
320 for occurrence in occurrences {
321 if occurrence > *after && !self.exceptions.contains(occurrence.date()) {
322 return Ok(Some(occurrence));
323 }
324 }
325
326 Ok(None)
327 }
328}
329
330// ========================================================================
331// Common Recurrence Patterns
332// ========================================================================
333
334impl RecurrencePattern {
335 pub fn daily(start: CalClock) -> Self {
336 Self::new(start, RecurrenceRule::daily())
337 }
338
339 pub fn weekly(start: CalClock) -> Self {
340 Self::new(start, RecurrenceRule::weekly())
341 }
342
343 pub fn monthly(start: CalClock) -> Self {
344 Self::new(start, RecurrenceRule::monthly())
345 }
346
347 pub fn yearly(start: CalClock) -> Self {
348 Self::new(start, RecurrenceRule::yearly())
349 }
350
351 pub fn business_days(start: CalClock) -> Self {
352 Self::new(start, RecurrenceRule::business_days())
353 }
354
355 pub fn weekends(start: CalClock) -> Self {
356 Self::new(start, RecurrenceRule::weekends())
357 }
358
359 pub fn first_of_month(start: CalClock) -> Self {
360 Self::new(start, RecurrenceRule::first_of_month())
361 }
362}