◐ Off-By-One · answer catalog

time-ical-recurring-events

2 answer(s)pythonpython3pythonpython3

dtstart=datetime(2025, 3, 3, 10, 0, 0, tzinfo=ZoneInfo("America/NewYork")),

📦 Source in repository (JSON)

Answer 1

The implementation is in ~/rrule_expander.py — a clean module that wraps Python's dateutil.rrule (which implements RFC 5545 Section 3.3.10) and adds full timezone, DST, and EXDATE support.

Core API — expand_rrule():

from datetime import datetime
from zoneinfo import ZoneInfo
from rrule_expander import expand_rrule, RecurrenceExpander

# Canonical problem: FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10
occurrences = expand_rrule(
    "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10",
    dtstart=datetime(2025, 1, 1, 10, 0, 0),
)
# Result: 10 datetimes on Jan 1, 6, 8, 13, 15, 20, 22, 27, 29 and Feb 3

# Timezone-aware with exclusions
occurrences = expand_rrule(
    "FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12",
    dtstart=datetime(2025, 3, 3, 10, 0, 0, tzinfo=ZoneInfo("America/New_York")),
    exdates=[datetime(2025, 3, 10, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))],
)

# Or use the RecurrenceExpander class
expander = RecurrenceExpander(
    dtstart=datetime(2025, 3, 10, 9, 0, 0),
    rrule_str="FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10",
    tz_name="America/New_York",
    exdates=[datetime(2025, 3, 17, 9, 0, 0)],
)
for occ in expander.expand():
    print(occ)

Key design decisions:


Evidence & signatures

**19 tests, all passing** (`cd ~ && python3 -m pytest test_rrule_expander.py -v`):

| Test | What it validates |
|------|-------------------|
| `test_basic_weekly` | `FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10` — the exact problem statement |
| `test_monthly_bymonthday` | `FREQ=MONTHLY;BYMONTHDAY=15;COUNT=4` |
| `test_yearly` | Annual recurrence |
| `test_interval` | `INTERVAL=2` — every other week |
| `test_until_bound_naive` | `UNTIL=` with naive datetimes |
| `test_until_bound_utc` | `UNTIL=` with UTC timezone-aware datetimes |
| `test_timezone_utc` | All UTC occurrences preserve tzinfo |
| `test_timezone_newyork` | America/New_York crossing Mar 9 DST boundary — 12 occurrences with offset change from -05:00 to -04:00 |
| `test_dst_spring_forward` | 2:30 AM daily across Mar 9 spring-forward — no crash, correct handling |
| `test_dst_fall_back` | 1:30 AM daily across Nov 2 fall-back — no duplicated occurrences |
| `test_daily_cross_dst` | 31 daily occurrences across March — Mar 8 at 10:00 EST, Mar 9 at 10:00 EDT |
| `test_exdate_exclusion` | 10 weekly → 8 after excluding 2 dates |
| `test_multiple_exdates` | 30 daily → 25 after excluding 5 dates |
| `test_exdate_timezone_aware` | EXDATE with timezone-aware datetimes |
| `test_naive_with_tz_parameter` | Naive dtstart + `tz_name="America/New_York"` → timezone-aware results |
| `test_bysetpos` | `BYSETPOS=-1` — last weekday of each month (Jan 31, Feb 28, Mar 31) |
| `test_bymonth_byday` | All Mondays in June across years |
| `test_expander_class` | `RecurrenceExpander` class API |
| `test_dst_detection` | `detect_dst_transitions()` finds both spring-forward and fall-back |

**Edge cases verified:**
1. **Spring-forward (2025-03-09)**: 2:30 AM doesn't exist → `dateutil` adjusts to the next valid time, producing a valid occurrence without crashing or skipping the day entirely.
2. **Fall-back (2025-11-02)**: 1:30 AM occurs twice → our expander produces exactly one occurrence, no duplication.
3. **Mixed timezone-aware/naive inputs**: Library raises clear errors when UNTIL is timezone-aware but DTSTART is naive (per RFC 5545).
4. **EXDATE at DST boundaries**: Excluding a date that crosses a DST boundary works correctly when both DTSTART and EXDATE are in the same timezone.

---
{"model": "claude-sonnet-4-20250514", "problem_class": "time-ical-recurring-events", "result": "passed", "tests": 19}

Answer 2

The implementation is in ~/rrule_expander.py — a clean module that wraps Python's dateutil.rrule (which implements RFC 5545 Section 3.3.10) and adds full timezone, DST, and EXDATE support.

Core API — expand_rrule():

from datetime import datetime
from zoneinfo import ZoneInfo
from rrule_expander import expand_rrule, RecurrenceExpander

# Canonical problem: FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10
occurrences = expand_rrule(
    "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10",
    dtstart=datetime(2025, 1, 1, 10, 0, 0),
)
# Result: 10 datetimes on Jan 1, 6, 8, 13, 15, 20, 22, 27, 29 and Feb 3

# Timezone-aware with exclusions
occurrences = expand_rrule(
    "FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12",
    dtstart=datetime(2025, 3, 3, 10, 0, 0, tzinfo=ZoneInfo("America/New_York")),
    exdates=[datetime(2025, 3, 10, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))],
)

# Or use the RecurrenceExpander class
expander = RecurrenceExpander(
    dtstart=datetime(2025, 3, 10, 9, 0, 0),
    rrule_str="FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10",
    tz_name="America/New_York",
    exdates=[datetime(2025, 3, 17, 9, 0, 0)],
)
for occ in expander.expand():
    print(occ)

Key design decisions:


Evidence & signatures

**19 tests, all passing** (`cd ~ && python3 -m pytest test_rrule_expander.py -v`):

| Test | What it validates |
|------|-------------------|
| `test_basic_weekly` | `FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10` — the exact problem statement |
| `test_monthly_bymonthday` | `FREQ=MONTHLY;BYMONTHDAY=15;COUNT=4` |
| `test_yearly` | Annual recurrence |
| `test_interval` | `INTERVAL=2` — every other week |
| `test_until_bound_naive` | `UNTIL=` with naive datetimes |
| `test_until_bound_utc` | `UNTIL=` with UTC timezone-aware datetimes |
| `test_timezone_utc` | All UTC occurrences preserve tzinfo |
| `test_timezone_newyork` | America/New_York crossing Mar 9 DST boundary — 12 occurrences with offset change from -05:00 to -04:00 |
| `test_dst_spring_forward` | 2:30 AM daily across Mar 9 spring-forward — no crash, correct handling |
| `test_dst_fall_back` | 1:30 AM daily across Nov 2 fall-back — no duplicated occurrences |
| `test_daily_cross_dst` | 31 daily occurrences across March — Mar 8 at 10:00 EST, Mar 9 at 10:00 EDT |
| `test_exdate_exclusion` | 10 weekly → 8 after excluding 2 dates |
| `test_multiple_exdates` | 30 daily → 25 after excluding 5 dates |
| `test_exdate_timezone_aware` | EXDATE with timezone-aware datetimes |
| `test_naive_with_tz_parameter` | Naive dtstart + `tz_name="America/New_York"` → timezone-aware results |
| `test_bysetpos` | `BYSETPOS=-1` — last weekday of each month (Jan 31, Feb 28, Mar 31) |
| `test_bymonth_byday` | All Mondays in June across years |
| `test_expander_class` | `RecurrenceExpander` class API |
| `test_dst_detection` | `detect_dst_transitions()` finds both spring-forward and fall-back |

**Edge cases verified:**
1. **Spring-forward (2025-03-09)**: 2:30 AM doesn't exist → `dateutil` adjusts to the next valid time, producing a valid occurrence without crashing or skipping the day entirely.
2. **Fall-back (2025-11-02)**: 1:30 AM occurs twice → our expander produces exactly one occurrence, no duplication.
3. **Mixed timezone-aware/naive inputs**: Library raises clear errors when UNTIL is timezone-aware but DTSTART is naive (per RFC 5545).
4. **EXDATE at DST boundaries**: Excluding a date that crosses a DST boundary works correctly when both DTSTART and EXDATE are in the same timezone.

---
{"model": "claude-sonnet-4-20250514", "problem_class": "time-ical-recurring-events", "result": "passed", "tests": 19}
Generated from the verified corpus · MIT licensedBack to the catalog