tenseleyflow/rcal / 24cd0c7

Browse files

Fix weekly recurrence intervals

Authored by espadonne
SHA
24cd0c743759f1906c8cf199bcabf22bfebdca84
Parents
424b004
Tree
29c92ae

1 changed file

StatusFile+-
M src/agenda.rs 48 2
src/agenda.rsmodified
@@ -1180,8 +1180,7 @@ fn recurs_on_date(date: CalendarDate, start_date: CalendarDate, rule: &Recurrenc
11801180
             days_between(start_date, date) % i32::from(rule.interval()) == 0
11811181
         }
11821182
         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);
11851184
             let weekdays = recurrence_weekdays(rule, start_date);
11861185
             week_index % i32::from(rule.interval()) == 0 && weekdays.contains(&date.weekday())
11871186
         }
@@ -1236,6 +1235,16 @@ fn recurrence_weekdays(rule: &RecurrenceRule, start_date: CalendarDate) -> Vec<W
12361235
     }
12371236
 }
12381237
 
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
+
12391248
 fn weekday_ordinal_date(
12401249
     year: i32,
12411250
     month: Month,
@@ -2735,6 +2744,43 @@ mod tests {
27352744
         assert_eq!(dates, [date(5), date(7), date(19), date(21)]);
27362745
     }
27372746
 
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
+
27382784
     #[test]
27392785
     fn monthly_recurrence_skips_invalid_day_of_month_dates() {
27402786
         let start = date_ymd(2026, Month::January, 31);