fix: correct recurrence rule parsing for schedules and calendar events (#29262)

* fix: correct recurrence rule parsing for schedules and calendar events

An automation set to repeat a limited number of times, say ten or a hundred, was treated as a one-shot and reported no repeat interval, because any count whose digits began with a one matched a text check for the one-shot case. The scheduler already answers that question correctly by asking the rule for its next two occurrences, so the text check is gone and the count is read as the number it is.

A recurrence rule that carries its start date on the same line as the repeat text kept that date when the automation was parsed, so the schedule ran from whatever date the rule happened to carry and ignored the start the user picked. The filter that drops the start date now splits the rule on any whitespace, the same way the rule parser itself does, so both agree on where one part of the rule ends and the next begins.

The same mismatch on the calendar path anchored a recurring event to the date inside its rule, so occurrences showed up before the event had begun and at the wrong time of day. That filter splits the rule the same way now, and the series starts at the event's own start.

All three come from one place, recurrence rules being matched and cut as text. Rules written across several lines, which is what the schedule and calendar editors produce, behave exactly as before.

* refac: name rrule token vars parts to match calendar.py
This commit is contained in:
Classic298
2026-09-21 08:44:13 -04:00
committed by GitHub
parent 0180efecf3
commit 438d9db8db
2 changed files with 5 additions and 7 deletions
+4 -6
View File
@@ -89,9 +89,9 @@ def _parse_rule(s: str, now: Optional[datetime] = None):
rule = rules[0]
start = rule._dtstart.replace(tzinfo=None)
anchor = now or datetime.now()
lines = s.splitlines()
stripped = '\n'.join(line for line in lines if not line.upper().startswith('DTSTART')) or s
has_dtstart = any(line.upper().startswith('DTSTART') for line in lines)
parts = s.split()
stripped = '\n'.join(part for part in parts if not part.upper().startswith('DTSTART')) or s
has_dtstart = any(part.upper().startswith('DTSTART') for part in parts)
step = {
SECONDLY: timedelta(seconds=rule._interval),
MINUTELY: timedelta(minutes=rule._interval),
@@ -183,9 +183,7 @@ def rrule_interval_seconds(s: str) -> Optional[int]:
Returns None for one-shot (COUNT=1) schedules or rules
with fewer than two future occurrences.
"""
if 'COUNT=1' in s:
return None
s = '\n'.join(line for line in s.splitlines() if not line.upper().startswith('DTSTART')) or s
s = '\n'.join(part for part in s.split() if not part.upper().startswith('DTSTART')) or s
now = datetime.now()
rule = _parse_rule(s, now)
first = rule.after(now)
+1 -1
View File
@@ -44,7 +44,7 @@ def expand_recurring_event(
original_start_ns = event_dict['start_at']
original_start = to_local_datetime(original_start_ns)
rule_str = '\n'.join(line for line in rrule_str.splitlines() if not line.upper().startswith('DTSTART')) or rrule_str
rule_str = '\n'.join(part for part in rrule_str.split() if not part.upper().startswith('DTSTART')) or rrule_str
try:
# Anchor to the event's real start so day-of-week / day-of-month are correct