@@ -1180,8 +1180,7 @@ fn recurs_on_date(date: CalendarDate, start_date: CalendarDate, rule: &Recurrenc |
| 1180 | 1180 | days_between(start_date, date) % i32::from(rule.interval()) == 0 |
| 1181 | 1181 | } |
| 1182 | 1182 | RecurrenceFrequency::Weekly => { |
| 1183 | | - let days = days_between(start_date, date); |
| 1184 | | - let week_index = days / 7; |
| 1183 | + let week_index = calendar_weeks_between(start_date, date); |
| 1185 | 1184 | let weekdays = recurrence_weekdays(rule, start_date); |
| 1186 | 1185 | week_index % i32::from(rule.interval()) == 0 && weekdays.contains(&date.weekday()) |
| 1187 | 1186 | } |
@@ -1236,6 +1235,16 @@ fn recurrence_weekdays(rule: &RecurrenceRule, start_date: CalendarDate) -> Vec<W |
| 1236 | 1235 | } |
| 1237 | 1236 | } |
| 1238 | 1237 | |
| 1238 | +fn calendar_weeks_between(start: CalendarDate, end: CalendarDate) -> i32 { |
| 1239 | + let start_week = sunday_of_week(start); |
| 1240 | + let end_week = sunday_of_week(end); |
| 1241 | + days_between(start_week, end_week) / 7 |
| 1242 | +} |
| 1243 | + |
| 1244 | +fn sunday_of_week(date: CalendarDate) -> CalendarDate { |
| 1245 | + date.add_days(-i32::from(date.weekday().number_days_from_sunday())) |
| 1246 | +} |
| 1247 | + |
| 1239 | 1248 | fn weekday_ordinal_date( |
| 1240 | 1249 | year: i32, |
| 1241 | 1250 | month: Month, |
@@ -2735,6 +2744,43 @@ mod tests { |
| 2735 | 2744 | assert_eq!(dates, [date(5), date(7), date(19), date(21)]); |
| 2736 | 2745 | } |
| 2737 | 2746 | |
| 2747 | + #[test] |
| 2748 | + fn weekly_interval_uses_calendar_weeks_not_rolling_start_windows() { |
| 2749 | + let start = date_ymd(2026, Month::April, 15); |
| 2750 | + let event = |
| 2751 | + Event::all_day("class", "CS412", start, source()).with_recurrence(RecurrenceRule { |
| 2752 | + frequency: RecurrenceFrequency::Weekly, |
| 2753 | + interval: 2, |
| 2754 | + end: RecurrenceEnd::Until(date_ymd(2026, Month::May, 15)), |
| 2755 | + weekdays: vec![Weekday::Monday, Weekday::Wednesday, Weekday::Friday], |
| 2756 | + monthly: None, |
| 2757 | + yearly: None, |
| 2758 | + }); |
| 2759 | + let source = InMemoryAgendaSource::with_events_and_holidays(vec![event], Vec::new()); |
| 2760 | + let range = DateRange::new( |
| 2761 | + date_ymd(2026, Month::April, 1), |
| 2762 | + date_ymd(2026, Month::May, 1), |
| 2763 | + ) |
| 2764 | + .expect("valid range"); |
| 2765 | + |
| 2766 | + let dates = source |
| 2767 | + .events_intersecting(range) |
| 2768 | + .into_iter() |
| 2769 | + .filter_map(|event| event.timing.date()) |
| 2770 | + .collect::<Vec<_>>(); |
| 2771 | + |
| 2772 | + assert_eq!( |
| 2773 | + dates, |
| 2774 | + [ |
| 2775 | + date_ymd(2026, Month::April, 15), |
| 2776 | + date_ymd(2026, Month::April, 17), |
| 2777 | + date_ymd(2026, Month::April, 27), |
| 2778 | + date_ymd(2026, Month::April, 29), |
| 2779 | + ] |
| 2780 | + ); |
| 2781 | + assert!(!dates.contains(&date_ymd(2026, Month::April, 20))); |
| 2782 | + } |
| 2783 | + |
| 2738 | 2784 | #[test] |
| 2739 | 2785 | fn monthly_recurrence_skips_invalid_day_of_month_dates() { |
| 2740 | 2786 | let start = date_ymd(2026, Month::January, 31); |