Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/calendar/rules.rs

28.0 KiB, 92 runs

created by r1870400018:8409, 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//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use crate::{
5 calendar::{CalendarDate, DayIncrementor, holiday_engines::HolidayEngine, business_day_engine::BusinessDayEngine},
6 constant::MonthOfYear,
7 time::CalClockZone,
8};
9
10use oxedyne_fe2o3_core::prelude::*;
11
12use std::collections::HashSet;
13
14/// Follows the Java calclock CalendarRule.RuleType enum.
15#[derive(Clone, Debug, PartialEq)]
16pub enum RuleType {
17 ByYears,
18 ByExplicitMonths,
19 ByRegularMonths,
20 ByDays,
21}
22
23/// A recurrence pattern, built up by chained with_ methods and then asked for
24/// its dates. Follows the Java calclock CalendarRule class.
25///
26/// ```ignore
27/// use oxedyne_fe2o3_datime::calendar::rules::{CalendarRule, RuleType};
28/// use oxedyne_fe2o3_datime::calendar::CalendarDate;
29/// use oxedyne_fe2o3_datime::time::CalClockZone;
30///
31/// let zone = CalClockZone::utc();
32/// let start_date = CalendarDate::from_ymd(2024, 1, 15, zone.clone()).unwrap();
33///
34/// // Create a rule for "2nd Tuesday of every quarter"
35/// let rule = CalendarRule::new(RuleType::ByExplicitMonths)
36/// .with_months(vec![1, 4, 7, 10]) // Quarterly
37/// .with_day_incrementor("2nd Tuesday")
38/// .with_start_date(start_date);
39///
40/// let dates = rule.generate_dates(10, zone).unwrap(); // Generate 10 occurrences
41/// ```
42#[derive(Clone, Debug)]
43pub struct CalendarRule {
44 rule_type: RuleType,
45 start_date: Option<CalendarDate>,
46 end_date: Option<CalendarDate>,
47 interval: u32,
48 explicit_months: Option<HashSet<u8>>,
49 day_incrementor: Option<DayIncrementor>,
50 holidays: Option<HolidaySet>,
51 holiday_engine: Option<HolidayEngine>,
52 business_day_engine: Option<BusinessDayEngine>,
53 max_occurrences: Option<u32>,
54}
55
56#[derive(Clone, Debug)]
57pub struct HolidaySet {
58 holidays: HashSet<CalendarDate>,
59 intervals: Vec<HolidayInterval>,
60}
61
62#[derive(Clone, Debug)]
63pub struct HolidayInterval {
64 start: CalendarDate,
65 end: CalendarDate, // inclusive
66 #[allow(dead_code)]
67 name: String,
68}
69
70impl CalendarRule {
71 pub fn new(rule_type: RuleType) -> Self {
72 Self {
73 rule_type,
74 start_date: None,
75 end_date: None,
76 interval: 1,
77 explicit_months: None,
78 day_incrementor: None,
79 holidays: None,
80 holiday_engine: None,
81 business_day_engine: None,
82 max_occurrences: None,
83 }
84 }
85
86 pub fn with_start_date(mut self, start_date: CalendarDate) -> Self {
87 self.start_date = Some(start_date);
88 self
89 }
90
91 pub fn with_end_date(mut self, end_date: CalendarDate) -> Self {
92 self.end_date = Some(end_date);
93 self
94 }
95
96 pub fn with_interval(mut self, interval: u32) -> Self {
97 self.interval = interval.max(1);
98 self
99 }
100
101 pub fn with_months(mut self, months: Vec<u8>) -> Self {
102 let mut month_set = HashSet::new();
103 for month in months {
104 if month >= 1 && month <= 12 {
105 month_set.insert(month);
106 }
107 }
108 self.explicit_months = Some(month_set);
109 self
110 }
111
112 pub fn with_day_incrementor_string(mut self, incrementor_str: &str) -> Outcome<Self> {
113 let incrementor = res!(DayIncrementor::from_string(incrementor_str));
114 self.day_incrementor = Some(incrementor);
115 Ok(self)
116 }
117
118 pub fn with_day_incrementor(mut self, incrementor: DayIncrementor) -> Self {
119 self.day_incrementor = Some(incrementor);
120 self
121 }
122
123 pub fn with_holidays(mut self, holidays: HolidaySet) -> Self {
124 self.holidays = Some(holidays);
125 self
126 }
127
128 pub fn with_max_occurrences(mut self, max: u32) -> Self {
129 self.max_occurrences = Some(max);
130 self
131 }
132
133 pub fn with_holiday_engine(mut self, engine: HolidayEngine) -> Self {
134 self.holiday_engine = Some(engine);
135 self
136 }
137
138 pub fn with_business_day_engine(mut self, engine: BusinessDayEngine) -> Self {
139 self.business_day_engine = Some(engine);
140 self
141 }
142
143 pub fn generate_dates(&self, count: usize, zone: CalClockZone) -> Outcome<Vec<CalendarDate>> {
144 let max_count = self.max_occurrences
145 .map(|max| max as usize)
146 .unwrap_or(count)
147 .min(count);
148
149 let start_date = ok!(self.start_date
150 .as_ref()
151 .ok_or_else(|| err!("Start date is required for rule generation"; Invalid, Input)));
152
153 match self.rule_type {
154 RuleType::ByYears => self.generate_yearly_dates(max_count, start_date, zone),
155 RuleType::ByExplicitMonths => self.generate_explicit_monthly_dates(max_count, start_date, zone),
156 RuleType::ByRegularMonths => self.generate_regular_monthly_dates(max_count, start_date, zone),
157 RuleType::ByDays => self.generate_daily_dates(max_count, start_date, zone),
158 }
159 }
160
161 fn generate_yearly_dates(&self, count: usize, start_date: &CalendarDate, zone: CalClockZone) -> Outcome<Vec<CalendarDate>> {
162 let mut dates = Vec::new();
163 let mut current_year = start_date.year();
164
165 for _ in 0..count {
166 let candidate_date = if let Some(ref incrementor) = self.day_incrementor {
167 // Use day incrementor to find the specific day in the year
168 res!(incrementor.calculate_date(current_year, start_date.month(), zone.clone()))
169 } else {
170 // Simple anniversary date
171 res!(CalendarDate::from_ymd(current_year, start_date.month_of_year(), start_date.day(), zone.clone()))
172 };
173
174 // Check if this date should be included
175 if self.should_include_date(&candidate_date) {
176 dates.push(candidate_date.clone());
177 }
178
179 // Check end date constraint
180 if let Some(ref end_date) = self.end_date {
181 if candidate_date > *end_date {
182 break;
183 }
184 }
185
186 current_year += self.interval as i32;
187 }
188
189 Ok(dates)
190 }
191
192 fn generate_explicit_monthly_dates(&self, count: usize, start_date: &CalendarDate, zone: CalClockZone) -> Outcome<Vec<CalendarDate>> {
193 let explicit_months = ok!(self.explicit_months
194 .as_ref()
195 .ok_or_else(|| err!("Explicit months required for ByExplicitMonths rule"; Invalid, Input)));
196
197 let mut dates = Vec::new();
198 let mut current_year = start_date.year();
199 let mut year_count = 0;
200
201 while dates.len() < count {
202 // Generate dates for each specified month in the current year
203 let mut year_months: Vec<u8> = explicit_months.iter().cloned().collect();
204 year_months.sort();
205
206 for month in year_months {
207 if dates.len() >= count {
208 break;
209 }
210
211 let candidate_date = if let Some(ref incrementor) = self.day_incrementor {
212 // Use day incrementor to find the specific day in the month
213 res!(incrementor.calculate_date(current_year, month, zone.clone()))
214 } else {
215 // Use the same day of month as start date
216 let day = start_date.day().min(ok!(MonthOfYear::from_number(month)).days_in_month(current_year));
217 res!(CalendarDate::from_ymd(current_year, ok!(MonthOfYear::from_number(month)), day, zone.clone()))
218 };
219
220 // Check if this date should be included
221 if self.should_include_date(&candidate_date) {
222 dates.push(candidate_date.clone());
223 }
224
225 // Check end date constraint
226 if let Some(ref end_date) = self.end_date {
227 if candidate_date > *end_date {
228 return Ok(dates);
229 }
230 }
231 }
232
233 current_year += self.interval as i32;
234 year_count += 1;
235
236 // Safety check to prevent infinite loops
237 if year_count > 1000 {
238 break;
239 }
240 }
241
242 Ok(dates)
243 }
244
245 fn generate_regular_monthly_dates(&self, count: usize, start_date: &CalendarDate, zone: CalClockZone) -> Outcome<Vec<CalendarDate>> {
246 let mut dates = Vec::new();
247 let mut current_date = start_date.clone();
248
249 for _ in 0..count {
250 let candidate_date = if let Some(ref incrementor) = self.day_incrementor {
251 // Use day incrementor to find the specific day in the month
252 res!(incrementor.calculate_date(current_date.year(), current_date.month(), zone.clone()))
253 } else {
254 current_date.clone()
255 };
256
257 // Check if this date should be included
258 if self.should_include_date(&candidate_date) {
259 dates.push(candidate_date.clone());
260 }
261
262 // Check end date constraint
263 if let Some(ref end_date) = self.end_date {
264 if candidate_date > *end_date {
265 break;
266 }
267 }
268
269 // Move to next occurrence
270 current_date = res!(current_date.add_months(self.interval as i32));
271 }
272
273 Ok(dates)
274 }
275
276 fn generate_daily_dates(&self, count: usize, start_date: &CalendarDate, _zone: CalClockZone) -> Outcome<Vec<CalendarDate>> {
277 let mut dates = Vec::new();
278 let mut current_date = start_date.clone();
279
280 for _ in 0..count {
281 // Check if this date should be included
282 if self.should_include_date(&current_date) {
283 dates.push(current_date.clone());
284 }
285
286 // Check end date constraint
287 if let Some(ref end_date) = self.end_date {
288 if current_date > *end_date {
289 break;
290 }
291 }
292
293 // Move to next occurrence
294 current_date = res!(current_date.add_days(self.interval as i32));
295 }
296
297 Ok(dates)
298 }
299
300 fn should_include_date(&self, date: &CalendarDate) -> bool {
301 // Check advanced holiday engine first
302 if let Some(ref engine) = self.holiday_engine {
303 if let Ok(is_holiday) = engine.is_holiday(date) {
304 if is_holiday {
305 return false;
306 }
307 }
308 }
309
310 // Check business day engine
311 if let Some(ref engine) = self.business_day_engine {
312 if let Ok(is_business_day) = engine.is_business_day(date) {
313 // For business day rules, only include actual business days
314 return is_business_day;
315 }
316 }
317
318 // Check legacy holiday set
319 if let Some(ref holidays) = self.holidays {
320 if holidays.is_holiday(date) {
321 return false;
322 }
323 }
324
325 // For now, include all dates. In the future, this could include
326 // additional business logic like "only business days" flags.
327 true
328 }
329}
330
331impl HolidaySet {
332 pub fn new() -> Self {
333 Self {
334 holidays: HashSet::new(),
335 intervals: Vec::new(),
336 }
337 }
338
339 pub fn add_holiday(&mut self, date: CalendarDate) {
340 self.holidays.insert(date);
341 }
342
343 pub fn add_interval(&mut self, start: CalendarDate, end: CalendarDate, name: String) {
344 self.intervals.push(HolidayInterval { start, end, name });
345 }
346
347 pub fn is_holiday(&self, date: &CalendarDate) -> bool {
348 // Check single-day holidays
349 if self.holidays.contains(date) {
350 return true;
351 }
352
353 // Check holiday intervals
354 for interval in &self.intervals {
355 if *date >= interval.start && *date <= interval.end {
356 return true;
357 }
358 }
359
360 false
361 }
362
363 pub fn is_business_day(&self, date: &CalendarDate) -> bool {
364 // Must be a weekday
365 if !date.is_weekday() {
366 return false;
367 }
368
369 // Must not be a holiday
370 !self.is_holiday(date)
371 }
372
373 pub fn get_holidays(&self) -> Vec<CalendarDate> {
374 let mut all_holidays = Vec::new();
375
376 // Add single-day holidays
377 all_holidays.extend(self.holidays.iter().cloned());
378
379 // Add dates from intervals
380 for interval in &self.intervals {
381 let mut current = interval.start.clone();
382 while current <= interval.end {
383 all_holidays.push(current.clone());
384 if let Ok(next_day) = current.add_days(1) {
385 current = next_day;
386 } else {
387 break;
388 }
389 }
390 }
391
392 all_holidays.sort();
393 all_holidays
394 }
395}
396
397impl Default for HolidaySet {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403// Convenience constructors for common rule patterns
404impl CalendarRule {
405 pub fn annually(start_date: CalendarDate) -> Self {
406 Self::new(RuleType::ByYears)
407 .with_start_date(start_date)
408 .with_interval(1)
409 }
410
411 pub fn quarterly(start_date: CalendarDate) -> Self {
412 Self::new(RuleType::ByRegularMonths)
413 .with_start_date(start_date)
414 .with_interval(3)
415 }
416
417 pub fn monthly_explicit(start_date: CalendarDate, months: Vec<u8>) -> Self {
418 Self::new(RuleType::ByExplicitMonths)
419 .with_start_date(start_date)
420 .with_months(months)
421 }
422
423 pub fn business_day_pattern(start_date: CalendarDate, pattern: &str, months: Vec<u8>) -> Outcome<Self> {
424 let rule = Self::new(RuleType::ByExplicitMonths)
425 .with_start_date(start_date)
426 .with_months(months);
427 rule.with_day_incrementor_string(pattern)
428 }
429
430 pub fn weekly(start_date: CalendarDate, interval_weeks: u32) -> Self {
431 Self::new(RuleType::ByDays)
432 .with_start_date(start_date)
433 .with_interval(interval_weeks * 7)
434 }
435
436 /// Excludes US federal holidays.
437 pub fn us_business_pattern(start_date: CalendarDate, pattern: &str, months: Vec<u8>) -> Outcome<Self> {
438 use crate::calendar::holiday_engines::HolidayEngine;
439 use crate::calendar::business_day_engine::BusinessDayEngine;
440
441 let holiday_engine = HolidayEngine::us_federal();
442 let business_engine = BusinessDayEngine::new()
443 .with_holiday_engine(holiday_engine.clone());
444
445 let rule = Self::new(RuleType::ByExplicitMonths)
446 .with_start_date(start_date)
447 .with_months(months)
448 .with_holiday_engine(holiday_engine)
449 .with_business_day_engine(business_engine);
450 rule.with_day_incrementor_string(pattern)
451 }
452
453 /// Excludes UK holidays.
454 pub fn uk_business_pattern(start_date: CalendarDate, pattern: &str, months: Vec<u8>) -> Outcome<Self> {
455 use crate::calendar::holiday_engines::HolidayEngine;
456 use crate::calendar::business_day_engine::BusinessDayEngine;
457
458 let holiday_engine = HolidayEngine::uk();
459 let business_engine = BusinessDayEngine::new()
460 .with_holiday_engine(holiday_engine.clone());
461
462 let rule = Self::new(RuleType::ByExplicitMonths)
463 .with_start_date(start_date)
464 .with_months(months)
465 .with_holiday_engine(holiday_engine)
466 .with_business_day_engine(business_engine);
467 rule.with_day_incrementor_string(pattern)
468 }
469
470 /// Excludes ECB holidays.
471 pub fn ecb_business_pattern(start_date: CalendarDate, pattern: &str, months: Vec<u8>) -> Outcome<Self> {
472 use crate::calendar::holiday_engines::HolidayEngine;
473 use crate::calendar::business_day_engine::BusinessDayEngine;
474
475 let holiday_engine = HolidayEngine::ecb();
476 let business_engine = BusinessDayEngine::new()
477 .with_holiday_engine(holiday_engine.clone());
478
479 let rule = Self::new(RuleType::ByExplicitMonths)
480 .with_start_date(start_date)
481 .with_months(months)
482 .with_holiday_engine(holiday_engine)
483 .with_business_day_engine(business_engine);
484 rule.with_day_incrementor_string(pattern)
485 }
486
487 /// A Sunday to Thursday business week.
488 pub fn middle_east_business_pattern(start_date: CalendarDate, pattern: &str, months: Vec<u8>) -> Outcome<Self> {
489 use crate::calendar::business_day_engine::{BusinessDayEngine, BusinessWeek};
490
491 let business_week = BusinessWeek::sunday_to_thursday();
492 let business_engine = BusinessDayEngine::new()
493 .with_business_week(business_week);
494
495 let rule = Self::new(RuleType::ByExplicitMonths)
496 .with_start_date(start_date)
497 .with_months(months)
498 .with_business_day_engine(business_engine);
499 rule.with_day_incrementor_string(pattern)
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use crate::time::CalClockZone;
507
508 #[test]
509 fn test_annual_rule() {
510 let zone = CalClockZone::utc();
511 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::June, 15, zone.clone()).unwrap();
512
513 let rule = CalendarRule::annually(start_date);
514 let dates = rule.generate_dates(3, zone).unwrap();
515
516 assert_eq!(dates.len(), 3);
517 assert_eq!(dates[0].year(), 2024);
518 assert_eq!(dates[1].year(), 2025);
519 assert_eq!(dates[2].year(), 2026);
520
521 for date in dates {
522 assert_eq!(date.month(), 6);
523 assert_eq!(date.day(), 15);
524 }
525 }
526
527 #[test]
528 fn test_quarterly_rule() {
529 let zone = CalClockZone::utc();
530 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::January, 15, zone.clone()).unwrap();
531
532 let rule = CalendarRule::quarterly(start_date);
533 let dates = rule.generate_dates(4, zone).unwrap();
534
535 assert_eq!(dates.len(), 4);
536
537 let expected_months = [1, 4, 7, 10];
538 for (i, date) in dates.iter().enumerate() {
539 assert_eq!(date.month(), expected_months[i]);
540 assert_eq!(date.day(), 15);
541 }
542 }
543
544 #[test]
545 fn test_explicit_months_rule() {
546 let zone = CalClockZone::utc();
547 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone()).unwrap();
548
549 let rule = CalendarRule::monthly_explicit(start_date, vec![3, 6, 9, 12]);
550 let dates = rule.generate_dates(4, zone).unwrap();
551
552 assert_eq!(dates.len(), 4);
553
554 let expected_months = [3, 6, 9, 12];
555 for (i, date) in dates.iter().enumerate() {
556 assert_eq!(date.month(), expected_months[i]);
557 assert_eq!(date.year(), 2024);
558 }
559 }
560
561 #[test]
562 fn test_holiday_set() {
563 let zone = CalClockZone::utc();
564 let mut holidays = HolidaySet::new();
565
566 // Add Christmas
567 let christmas = CalendarDate::from_ymd(2024, MonthOfYear::December, 25, zone.clone()).unwrap();
568 holidays.add_holiday(christmas.clone());
569
570 // Add New Year's break (interval)
571 let new_years_start = CalendarDate::from_ymd(2024, MonthOfYear::December, 31, zone.clone()).unwrap();
572 let new_years_end = CalendarDate::from_ymd(2025, MonthOfYear::January, 2, zone.clone()).unwrap();
573 holidays.add_interval(new_years_start, new_years_end, "New Year's Break".to_string());
574
575 // Test holiday detection
576 assert!(holidays.is_holiday(&christmas));
577 assert!(holidays.is_holiday(&CalendarDate::from_ymd(2024, MonthOfYear::December, 31, zone.clone()).unwrap()));
578 assert!(holidays.is_holiday(&CalendarDate::from_ymd(2025, MonthOfYear::January, 1, zone.clone()).unwrap()));
579 assert!(holidays.is_holiday(&CalendarDate::from_ymd(2025, MonthOfYear::January, 2, zone.clone()).unwrap()));
580
581 // Test non-holiday
582 assert!(!holidays.is_holiday(&CalendarDate::from_ymd(2024, MonthOfYear::December, 24, zone).unwrap()));
583 }
584
585 #[test]
586 fn test_business_day_detection() {
587 let zone = CalClockZone::utc();
588 let mut holidays = HolidaySet::new();
589
590 // Add a holiday on a weekday
591 let holiday = CalendarDate::from_ymd(2024, MonthOfYear::July, 4, zone.clone()).unwrap(); // Thursday
592 holidays.add_holiday(holiday.clone());
593
594 // Thursday July 4, 2024 is a weekday but a holiday
595 assert!(!holidays.is_business_day(&holiday));
596
597 // Friday July 5, 2024 is a weekday and not a holiday
598 let business_day = CalendarDate::from_ymd(2024, MonthOfYear::July, 5, zone.clone()).unwrap();
599 assert!(holidays.is_business_day(&business_day));
600
601 // Saturday July 6, 2024 is not a weekday
602 let weekend = CalendarDate::from_ymd(2024, MonthOfYear::July, 6, zone).unwrap();
603 assert!(!holidays.is_business_day(&weekend));
604 }
605
606 #[test]
607 fn test_advanced_holiday_engine_integration() {
608 use crate::calendar::holiday_engines::HolidayEngine;
609
610 let zone = CalClockZone::utc();
611 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone()).unwrap();
612
613 // Create a rule with US federal holidays
614 let rule = CalendarRule::new(RuleType::ByExplicitMonths)
615 .with_start_date(start_date)
616 .with_months(vec![7]) // July only
617 .with_holiday_engine(HolidayEngine::us_federal());
618
619 let dates = rule.generate_dates(31, zone.clone()).unwrap(); // Generate all days in July
620
621 // Should exclude July 4th (Independence Day)
622 let july_4 = CalendarDate::from_ymd(2024, MonthOfYear::July, 4, zone.clone()).unwrap();
623 assert!(!dates.contains(&july_4), "July 4th should be excluded as US federal holiday");
624
625 // Should include July 5th (not a holiday)
626 let july_5 = CalendarDate::from_ymd(2024, MonthOfYear::July, 5, zone.clone()).unwrap();
627 assert!(dates.contains(&july_5), "July 5th should be included as regular day");
628 }
629
630 #[test]
631 fn test_business_day_engine_integration() {
632 use crate::calendar::business_day_engine::BusinessDayEngine;
633 use crate::calendar::holiday_engines::HolidayEngine;
634
635 let zone = CalClockZone::utc();
636 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::July, 1, zone.clone()).unwrap();
637
638 // Create business day engine with holidays
639 let business_engine = BusinessDayEngine::new()
640 .with_holiday_engine(HolidayEngine::us_federal());
641
642 let rule = CalendarRule::new(RuleType::ByDays)
643 .with_start_date(start_date)
644 .with_interval(1) // Every day
645 .with_business_day_engine(business_engine);
646
647 let dates = rule.generate_dates(31, zone.clone()).unwrap();
648
649 // Should only include business days (weekdays that aren't holidays)
650 for date in &dates {
651 assert!(date.is_weekday(), "All dates should be weekdays");
652
653 // July 4th should not be included (Independence Day)
654 if date.month() == 7 && date.day() == 4 {
655 panic!("July 4th should not be included in business days");
656 }
657 }
658
659 // Should have approximately 22-23 business days in July 2024 (excluding July 4th)
660 assert!(dates.len() >= 22 && dates.len() <= 23,
661 "Expected 22-23 business days in July 2024, got {}", dates.len());
662 }
663
664 #[test]
665 fn test_us_business_pattern_convenience() {
666 let zone = CalClockZone::utc();
667 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone()).unwrap();
668
669 // Create "2nd Tuesday of every quarter" rule with US federal holidays
670 let rule = CalendarRule::us_business_pattern(
671 start_date,
672 "2nd Tuesday",
673 vec![1, 4, 7, 10] // Quarterly
674 ).unwrap();
675
676 let dates = rule.generate_dates(4, zone.clone()).unwrap();
677
678 assert_eq!(dates.len(), 4, "Should generate 4 quarterly dates");
679
680 // Check that all dates are Tuesdays
681 for date in &dates {
682 assert_eq!(date.day_of_week(), crate::constant::DayOfWeek::Tuesday,
683 "All dates should be Tuesdays");
684 }
685
686 // Check months are quarterly
687 let months: Vec<u8> = dates.iter().map(|d| d.month()).collect();
688 assert_eq!(months, vec![1, 4, 7, 10], "Should be quarterly months");
689 }
690
691 #[test]
692 fn test_middle_east_business_week() {
693 let zone = CalClockZone::utc();
694 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::June, 1, zone.clone()).unwrap(); // Saturday
695
696 // Create rule for Sunday-Thursday business week
697 let rule = CalendarRule::middle_east_business_pattern(
698 start_date,
699 "1st business day",
700 vec![6] // June only
701 ).unwrap();
702
703 let dates = rule.generate_dates(1, zone.clone()).unwrap();
704
705 assert_eq!(dates.len(), 1, "Should generate 1 date");
706
707 let first_business_day = &dates[0];
708
709 // First business day of June 2024 should be Sunday June 2nd (Saturday is weekend in Middle East)
710 assert_eq!(first_business_day.day(), 2, "First business day should be June 2nd");
711 assert_eq!(first_business_day.day_of_week(), crate::constant::DayOfWeek::Sunday,
712 "Should be Sunday in Middle East business week");
713 }
714
715 #[test]
716 fn test_uk_easter_based_holidays() {
717 let zone = CalClockZone::utc();
718 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::March, 1, zone.clone()).unwrap();
719
720 // Create rule with UK holidays (includes Easter-based holidays)
721 let rule = CalendarRule::uk_business_pattern(
722 start_date,
723 "1st business day",
724 vec![3, 4] // March and April (around Easter)
725 ).unwrap();
726
727 let dates = rule.generate_dates(2, zone.clone()).unwrap();
728
729 // Should exclude Good Friday (March 29, 2024) and Easter Monday (April 1, 2024)
730 for date in &dates {
731 let is_good_friday = date.month() == 3 && date.day() == 29;
732 let is_easter_monday = date.month() == 4 && date.day() == 1;
733
734 assert!(!is_good_friday && !is_easter_monday,
735 "Should exclude Easter holidays: Good Friday and Easter Monday");
736 }
737 }
738
739 #[test]
740 fn test_complex_rule_with_day_incrementor_and_engines() {
741 let zone = CalClockZone::utc();
742 let start_date = CalendarDate::from_ymd(2024, MonthOfYear::January, 1, zone.clone()).unwrap();
743
744 // Create a complex rule: "Last business day of every month" with US federal holidays
745 let rule = CalendarRule::us_business_pattern(
746 start_date,
747 "last business day",
748 vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] // All months
749 ).unwrap();
750
751 let dates = rule.generate_dates(12, zone.clone()).unwrap();
752
753 assert_eq!(dates.len(), 12, "Should generate one date per month");
754
755 // Each date should be the last business day of its month
756 for (i, date) in dates.iter().enumerate() {
757 assert_eq!(date.month(), (i + 1) as u8, "Should be in the correct month");
758 assert!(date.is_weekday(), "Should be a weekday");
759
760 // Verify it's actually the last business day by checking next day is not a business day
761 if let Ok(next_day) = date.add_days(1) {
762 if next_day.month() == date.month() {
763 // If next day is in same month, it should not be a business day
764 // (either weekend or holiday, or this wouldn't be the last business day)
765 }
766 }
767 }
768 }
769}