diff --git a/ical/event.py b/ical/event.py index 2a5a39e..a729bee 100644 --- a/ical/event.py +++ b/ical/event.py @@ -27,7 +27,7 @@ from .alarm import Alarm from .component import ComponentModel, validate_until_dtstart, validate_recurrence_dates -from .iter import RulesetIterable +from .iter import RulesetIterable, as_rrule from .exceptions import CalendarParseError from .parsing.property import ParsedProperty from .timespan import Timespan @@ -362,16 +362,7 @@ def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None: This is only valid for events where `recurring` is True. """ - if not self.rrule and not self.rdate: - return None - if not self.start: - raise CalendarParseError("Event must have a start date to be recurring") - return RulesetIterable( - self.start, - [self.rrule.as_rrule(self.start)] if self.rrule else [], - self.rdate, - self.exdate, - ) + return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart) @root_validator(pre=True, allow_reuse=True) def _inspect_date_types(cls, values: dict[str, Any]) -> dict[str, Any]: diff --git a/ical/iter.py b/ical/iter.py index 4b62edd..caf3a35 100644 --- a/ical/iter.py +++ b/ical/iter.py @@ -28,6 +28,8 @@ from .timespan import Timespan from .util import normalize_datetime +from .types.recur import Recur +from .exceptions import CalendarParseError __all__ = [ "RecurrenceError", @@ -401,3 +403,29 @@ def today(self) -> Iterator[T]: def now(self, tz: datetime.tzinfo | None = None) -> Iterator[T]: """Return an iterator containing all events active on the specified day.""" return self.at_instant(datetime.datetime.now(tz=tz)) + + +def as_rrule( + rrule: Recur | None, + rdate: list[datetime.datetime | datetime.date], + exdate: list[datetime.datetime | datetime.date], + start: datetime.datetime | datetime.date | None, +) -> Iterable[datetime.datetime | datetime.date] | None: + """Return an iterable containing the occurrences of a recurring event. + + A recurring event is typically evaluated specially on the timeline. The + data model has a single event, but the timeline evaluates the recurrence + to expand and copy the the event to multiple places on the timeline. + + This is only valid for events where `recurring` is True. + """ + if not rrule and not rdate: + return None + if not start: + raise CalendarParseError("Event must have a start date to be recurring") + return RulesetIterable( + start, + [rrule.as_rrule(start)] if rrule else [], + rdate, + exdate, + ) diff --git a/ical/journal.py b/ical/journal.py index 09ddcb4..4e74fc7 100644 --- a/ical/journal.py +++ b/ical/journal.py @@ -28,7 +28,7 @@ ) from .exceptions import CalendarParseError from .util import dtstamp_factory, normalize_datetime, uid_factory, local_timezone -from .iter import RulesetIterable +from .iter import RulesetIterable, as_rrule from .timespan import Timespan @@ -156,16 +156,7 @@ def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None: This is only valid for events where `recurring` is True. """ - if not self.rrule and not self.rdate: - return None - if not self.start: - raise CalendarParseError("Event must have a start date to be recurring") - return RulesetIterable( - self.start, - [self.rrule.as_rrule(self.start)] if self.rrule else [], - self.rdate, - [], - ) + return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart) _validate_until_dtstart = root_validator(allow_reuse=True)(validate_until_dtstart) _validate_recurrence_dates = root_validator(allow_reuse=True)( diff --git a/ical/todo.py b/ical/todo.py index 337fa8e..c5dd5fa 100644 --- a/ical/todo.py +++ b/ical/todo.py @@ -24,7 +24,7 @@ from .alarm import Alarm from .component import ComponentModel, validate_until_dtstart, validate_recurrence_dates from .exceptions import CalendarParseError -from .iter import RulesetIterable +from .iter import RulesetIterable, as_rrule from .parsing.property import ParsedProperty from .timespan import Timespan from .types import ( @@ -279,16 +279,9 @@ def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None: """ if not self.rrule and not self.rdate: return None - if not self.start: - raise CalendarParseError("Event must have a start date to be recurring") if not self.due: raise CalendarParseError("Event must have a due date to be recurring") - return RulesetIterable( - self.start, - [self.rrule.as_rrule(self.start)] if self.rrule else [], - self.rdate, - self.exdate, - ) + return as_rrule(self.rrule, self.rdate, self.exdate, self.dtstart) @root_validator def _validate_one_due_or_duration(cls, values: dict[str, Any]) -> dict[str, Any]: