diff --git a/ical/list.py b/ical/list.py index db5b32f..61c982f 100644 --- a/ical/list.py +++ b/ical/list.py @@ -36,12 +36,14 @@ class RecurAdapter: def __init__(self, todo: Todo, tzinfo: datetime.tzinfo | None = None): """Initialize the RecurAdapter.""" self._todo = todo + if todo.computed_duration is None: + raise ValueError("Recurring todo must have a duration") self._duration = todo.computed_duration self._tzinfo = tzinfo def get( self, dtstart: datetime.datetime | datetime.date - ) -> SortableItem[datetime.datetime, Todo]: + ) -> SortableItem[datetime.datetime | datetime.date | None, Todo]: """Return a lazy sortable item.""" recur_id_dt = dtstart @@ -77,11 +79,11 @@ def _todos_by_uid(todos: list[Todo]) -> dict[str, list[Todo]]: def _todo_iterable( iterable: list[Todo], tzinfo: datetime.tzinfo -) -> Iterable[SortableItem[datetime.datetime, Todo]]: +) -> Iterable[SortableItem[datetime.datetime | datetime.date | None, Todo]]: """Create a sorted iterable from the list of events.""" def sortable_items() -> ( - Generator[SortableItem[datetime.datetime, Todo], None, None] + Generator[SortableItem[datetime.datetime | datetime.date | None, Todo], None, None] ): for todo in iterable: if todo.recurring: @@ -111,15 +113,18 @@ def _pick_todo(todos: list[Todo], tzinfo: datetime.tzinfo) -> Todo: iters.append(RecurIterable(RecurAdapter(todo, tzinfo=tzinfo).get, recur)) root_iter = MergedIterable(iters) + # Pick the first todo that hasn't started yet based on its dtstart now = datetime.datetime.now(tzinfo) last: Todo | None = None - todo = next(iter(root_iter)) - for todo in root_iter: - if todo.item.start_datetime > now: + + it = iter(root_iter) + last = next(it, None) + while cur := next(it, None): + if cur.item.start_datetime is None or cur.item.start_datetime > now: break - last = todo.item - return last + last = cur + return last.item if last is not None else None def todo_list_view( diff --git a/ical/todo.py b/ical/todo.py index 28f9d02..a8baea2 100644 --- a/ical/todo.py +++ b/ical/todo.py @@ -89,15 +89,6 @@ class Todo(ComponentModel): duration: Optional[datetime.timedelta] = None """The duration of the item as an alternative to an explicit end date/time.""" - exdate: list[Union[datetime.datetime, datetime.date]] = Field(default_factory=list) - """Defines the list of exceptions for recurring todo item. - - The exception dates are used in computing the recurrence set. The recurrence set is - the complete set of recurrence instances for a calendar component (based on rrule, rdate, - exdate). The recurrence set is generated by gathering the rrule and rdate properties - then excluding any times specified by exdate. - """ - geo: Optional[Geo] = None """Specifies a latitude and longitude global position for the activity.""" @@ -212,29 +203,9 @@ def start_datetime(self) -> datetime.datetime | None: @property def computed_duration(self) -> datetime.timedelta | None: """Return the event duration.""" - if self.due is None: + if self.due is None or self.dtstart is None: return None - return self.due - self.start - - def __lt__(self, other: Any) -> bool: - if not isinstance(other, Todo): - return NotImplemented - return self.due < other.due - - def __gt__(self, other: Any) -> bool: - if not isinstance(other, Todo): - return NotImplemented - return self.due > other.due - - def __le__(self, other: Any) -> bool: - if not isinstance(other, Todo): - return NotImplemented - return self.due <= other.due - - def __ge__(self, other: Any) -> bool: - if not isinstance(other, Todo): - return NotImplemented - return self.due >= other.due + return self.due - self.dtstart @property def recurring(self) -> bool: @@ -258,7 +229,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: + if not (self.rrule or self.rdate) or not self.start: return None return RulesetIterable( self.start,