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 | |
| 10 | use crate::{ |
| 11 | calendar::CalendarDate, |
| 12 | constant::{DayOfWeek, MonthOfYear}, |
| 13 | time::CalClock, |
| 14 | }; |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | |
| 18 | use std::collections::HashSet; |
| 19 | |
| 20 | #[derive(Clone, Debug, PartialEq)] |
| 21 | pub enum Frequency { |
| 22 | Daily, |
| 23 | Weekly, |
| 24 | Monthly, |
| 25 | Yearly, |
| 26 | Hourly, |
| 27 | Minutely, |
| 28 | Secondly, |
| 29 | } |
| 30 | |
| 31 | #[derive(Clone, Debug)] |
| 32 | pub 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 | |
| 46 | impl 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(¤t)) { |
| 186 | occurrences.push(current.clone()); |
| 187 | } |
| 188 | |
| 189 | // Move to the next potential occurrence |
| 190 | current = res!(self.advance(¤t)); |
| 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)] |
| 262 | pub struct RecurrencePattern { |
| 263 | start: CalClock, |
| 264 | rule: RecurrenceRule, |
| 265 | exceptions: HashSet<CalendarDate>, // dates to skip |
| 266 | } |
| 267 | |
| 268 | impl 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 | |
| 334 | impl 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 | } |