diff --git a/tests/test_recurrence_weekly.py b/tests/test_recurrence_weekly.py index 10ac59d..4ffd49a 100644 --- a/tests/test_recurrence_weekly.py +++ b/tests/test_recurrence_weekly.py @@ -19,10 +19,12 @@ def _d(date: str) -> datetime.date: # 2026-09-01 is a TUESDAY (weekday 1). -# A weekly Mon/Wed/Fri (0,2,4) rule starting Tue 2026-09-01, interval 1, -# fires: Sep 1 (Tue), Sep 2 (Wed), Sep 4 (Fri), then each following week -# on Mon/Wed/Fri: Sep 7, 9, 11, 14, 16, 18, 21, 23, 25, 28, 30, Oct 2... -# The 8-week hand-checked sequence below is built from this anchor. +# Anchor-week arithmetic (start_date = Tue Sep 1): week 0 spans +# Sep 1-7, week 1 spans Sep 8-14, week 2 spans Sep 15-21, and so on — +# whole weeks elapsed from the anchor, NOT calendar-Sunday weeks. +# A weekly Mon/Wed/Fri (0,2,4) rule at interval 1 anchored here fires +# on Wed Sep 2, Fri Sep 4 (both week 0), then Mon Sep 7, Wed Sep 9, +# Fri Sep 11 (week 1), Mon Sep 14... (week 2), and so on. def test_weekly_fires_exactly_on_set_weekdays() -> None: @@ -153,18 +155,23 @@ def test_hand_checked_8_week_sequence_weekly_mon_wed_fri() -> None: """The done-condition's hand-checked sequence: 8 weeks of a Mon/Wed/Fri rule anchored at Tue 2026-09-01, interval 1. - Week 0 (Sep 1-6): Wed 2, Fri 4 (Mon 31 Aug is pre-start) - Week 1 (Sep 7-13): Mon 7, Wed 9, Fri 11 - Week 2 (Sep 14-20): Mon 14, Wed 16, Fri 18 - Week 3 (Sep 21-27): Mon 21, Wed 23, Fri 25 - Week 4 (Sep 28-Oct 4): Mon 28, Wed 30, Fri 2 (Oct) - Week 5 (Oct 5-11): Mon 5, Wed 7, Fri 9 - Week 6 (Oct 12-18): Mon 12, Wed 14, Fri 16 - Week 7 (Oct 19-25): Mon 19, Wed 21, Fri 23 + Anchor weeks from Tue Sep 1 (week 0 = Sep 1-7, week 1 = Sep 8-14, + ... week 7 = Oct 20-26). Mon/Wed/Fri firings (interval 1, so every + in-set weekday fires regardless of week): + Week 0 (Sep 1-7): Wed 2, Fri 4 (Mon Sep 7 is ALSO week 0: + 6 elapsed days // 7 = 0 — the anchor week + boundary sits on Sep 8, not on the Monday) + Week 1 (Sep 8-14): Wed 9, Fri 11, Mon 14 (13 elapsed days // 7 = 1) + Week 2 (Sep 15-21): Wed 16, Fri 18, Mon 21 + Week 3 (Sep 22-28): Wed 23, Fri 25, Mon 28 + Week 4 (Sep 29-Oct 5): Wed 30, Fri 2 (Oct), Mon 5 (Oct) + Week 5 (Oct 6-12): Wed 7, Fri 9, Mon 12 + Week 6 (Oct 13-19): Wed 14, Fri 16, Mon 19 + Week 7 (Oct 20-26): Wed 21, Fri 23, Mon 26 """ rule = ScheduleRule( rule_type=RuleType.WEEKLY, weekday_set={0, 2, 4}, - start_date="2026-09-01", end_date="2026-10-25", + start_date="2026-09-01", end_date="2026-10-26", ) expected = [ "2026-09-02", "2026-09-04", @@ -174,17 +181,18 @@ def test_hand_checked_8_week_sequence_weekly_mon_wed_fri() -> None: "2026-09-28", "2026-09-30", "2026-10-02", "2026-10-05", "2026-10-07", "2026-10-09", "2026-10-12", "2026-10-14", "2026-10-16", - "2026-10-19", "2026-10-21", "2026-10-23", + "2026-10-19", "2026-10-21", "2026-10-23", "2026-10-26", ] - actual = occurrences_between(rule, "2026-09-01", "2026-10-25") + actual = occurrences_between(rule, "2026-09-01", "2026-10-26") assert actual == expected, ( f"diff: {set(expected) ^ set(actual)}" ) def test_hand_checked_8_week_sequence_weekly_interval_2_mondays() -> None: - """8 weeks of a Monday rule at interval 2, anchored Mon 2026-09-07: - fires in weeks 0, 2, 4, 6 -> Sep 7, Sep 21, Oct 5, Oct 19.""" + """8 weeks of a Monday rule at interval 2, anchored Mon 2026-09-07 + (week 0 = Sep 7-13): fires in weeks 0, 2, 4, 6 -> Mon Sep 7, Mon Sep 21, + Mon Oct 5, Mon Oct 19.""" rule = ScheduleRule( rule_type=RuleType.WEEKLY, weekday_set={0}, interval=2, start_date="2026-09-07", end_date="2026-10-31", @@ -200,7 +208,9 @@ def test_custom_days_hand_checked_8_week_sequence() -> None: rule_type=RuleType.CUSTOM_DAYS, weekday_set={1, 3, 6}, start_date="2026-09-07", ) - actual = occurrences_between(rule, "2026-09-07", "2026-10-27") + # Anchor weeks from Mon Sep 7 (w0 = Sep 7-13 ... w7 = Oct 26-Nov 1): + # Tue/Thu/Sun firings per week, 8 weeks fully covered. + actual = occurrences_between(rule, "2026-09-07", "2026-11-01") assert actual == [ "2026-09-08", "2026-09-10", "2026-09-13", "2026-09-15", "2026-09-17", "2026-09-20", @@ -208,7 +218,8 @@ def test_custom_days_hand_checked_8_week_sequence() -> None: "2026-09-29", "2026-10-01", "2026-10-04", "2026-10-06", "2026-10-08", "2026-10-11", "2026-10-13", "2026-10-15", "2026-10-18", - "2026-10-20", "2026-10-22", "2026-10-25", "2026-10-27", + "2026-10-20", "2026-10-22", "2026-10-25", + "2026-10-27", "2026-10-29", "2026-11-01", ] @@ -266,4 +277,23 @@ def test_occurrences_between_rejects_inverted_bounds() -> None: rule_type=RuleType.WEEKLY, weekday_set={0}, start_date="2026-09-01" ) with pytest.raises(RuleValidationError, match="on or after"): - occurrences_between(rule, "2026-10-10", "2026-09-01") \ No newline at end of file + occurrences_between(rule, "2026-10-10", "2026-09-01") + +def test_weekly_interval_2_non_monday_anchor_boundary() -> None: + """The anchor week is anchored at start_date, NOT a calendar week: + a Tuesday-anchored Monday rule fires on Mon Sep 7 (week 0 extends + through Sep 7), skips Mon Sep 14 (week 1), fires Mon Sep 21 (week 2).""" + rule = ScheduleRule( + rule_type=RuleType.WEEKLY, weekday_set={0}, interval=2, + start_date="2026-09-01", # a TUESDAY anchor + ) + # Sep 7 - Sep 1 = 6 days -> week 0 (even): fires. + assert occurs_on(rule, _d("2026-09-07")) is True + # Sep 14: 13 days -> week 1 (odd): skipped. + assert occurs_on(rule, _d("2026-09-14")) is False + # Sep 21: 20 days -> week 2 (even): fires. + assert occurs_on(rule, _d("2026-09-21")) is True + # Sep 28: 27 days -> week 3 (odd): skipped. + assert occurs_on(rule, _d("2026-09-28")) is False + # Oct 5: 34 days -> week 4 (even): fires. + assert occurs_on(rule, _d("2026-10-05")) is True \ No newline at end of file