Skip to content

Documentation for the Timeline

From the VCalendar it is very easy to create a Timeline object. This will show you all the components you have in a specific time frame.

Timeline

This class is a wrapper to make it easy to see what the order of each component based on the start date.

Inside this class there are multiple methods to iterate over all the components present. However, one should note that often the recurrence properties for components specify only a lower bound and will therefore proceed to infinity. To prevent us having an infinite list of items to iterate on, we define upper and lower bounds. You should set these upper bounds to the absolute minimum start date and absolute maximum end date that you would ever need. So if you need to do 10 different queries, this start and end date should range all of these, so it doesn't need to compute the list of components in the range over and over again. The functions themselves(e.g. :function:self.includes and :function:self.intersects) help you to limit the exact range you want to return components for.

Parameters:

Name Type Description Default
v_calendar VCalendar

The VCalendar object we are iterating over.

required
start_date Optional[DateTime]

The minimum ending date of each event that is returned inside this timeline.

None
end_date Optional[DateTime]

The maximum starting date of each event that is return inside this timeline.

None
Source code in ical_library/timeline.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class Timeline:
    """
    This class is a wrapper to make it easy to see what the order of each component based on the start date.

    Inside this class there are multiple methods to iterate over all the components present. However, one should note
    that often the recurrence properties for components specify only a lower bound and will therefore proceed to
    infinity. To prevent us having an infinite list of items to iterate on, we define upper and lower bounds.
    You should set these upper bounds to the absolute minimum start date and absolute maximum end date that you would
    ever need. So if you need to do 10 different queries, this start and end date should range all of these, so it
    doesn't need to compute the list of components in the range over and over again. The functions themselves(e.g.
    :function:`self.includes` and :function:`self.intersects`) help you to limit the exact range you want to return
    components for.

    :param v_calendar: The VCalendar object we are iterating over.
    :param start_date: The minimum ending date of each event that is returned inside this timeline.
    :param end_date: The maximum starting date of each event that is return inside this timeline.
    """

    def __init__(
        self, v_calendar: VCalendar, start_date: Optional[DateTime] = None, end_date: Optional[DateTime] = None
    ):
        self.v_calendar: VCalendar = v_calendar
        self._start_date: DateTime = start_date or DateTime(1970, 1, 1)
        self._end_date: DateTime = end_date or DateTime(2100, 1, 1)

    def __repr__(self) -> str:
        return f"Timeline({self._start_date}, {self._end_date})"

    @property
    def start_date(self) -> DateTime:
        """Return the start date of the timeline. No event should end before this value."""
        return self._start_date

    @start_date.setter
    def start_date(self, value) -> None:
        """Set the start date of the timeline."""
        self._start_date = value
        self.get_timespan.cache_clear()

    @property
    def end_date(self) -> DateTime:
        """Return the end date of the timeline. No event should start before this value."""
        return self._end_date

    @end_date.setter
    def end_date(self, value) -> None:
        """Set the end date of the timeline."""
        self._end_date = value
        self.get_timespan.cache_clear()

    @instance_lru_cache()
    def get_timespan(self) -> Timespan:
        """Return the start and end date as a Timespan."""
        return Timespan(self.start_date, self.end_date)

    @staticmethod
    def __get_items_to_exclude_from_recurrence(
        all_components: List[Component],
    ) -> Dict[str, Union[List[Date], List[DateTime]]]:
        """
        Deduplicate recurring components. Sometimes it happens that recurring events are changed and this will cause
        them to both be present as a standard component and in the recurrence.
        :param all_components: The list of all component children of the VCalendar instance.
        :return: A deduplicated list of components.
        """
        start_date_to_timespan_dict: Dict[str, Union[List[Date], List[DateTime]]] = defaultdict(list)
        for component in all_components:
            if isinstance(component, AbstractComponentWithRecurringProperties) and component.recurrence_id is not None:
                start_date_to_timespan_dict[component.uid.value].append(component.recurrence_id.datetime_or_date_value)
        return start_date_to_timespan_dict

    def __explode_recurring_components(self) -> List[TimespanWithParent]:
        """
        Get a de-duplicated list of all components with a start date, including the recurring components. This means
        that we add all child component of the :class:`VCalendar` (except for the :class:`VTimeZone` instances) to a
        list and then add all extra occurrences (as recurring components) according to the recurrence properties:
        :class:`RRule`, :class:`RDate` and :class:`EXDate`.
        :return: A de-duplicated list of all components, including the recurring occurrences of the components.
        """
        list_of_timestamps_with_parents: List[TimespanWithParent] = []
        all_children = self.v_calendar.children
        uid_to_datetime_to_exclude = self.__get_items_to_exclude_from_recurrence(all_children)
        for c in all_children:
            # Do some initial filtering.
            if isinstance(c, AbstractComponentWithRecurringProperties):
                if c.max_recurring_timespan.intersects(self.get_timespan()):
                    values_to_exclude = uid_to_datetime_to_exclude[c.uid.value]  # .get defaults to None.
                    list_of_timestamps_with_parents.extend(
                        c.expand_component_in_range(self.get_timespan(), values_to_exclude)
                    )
            elif isinstance(c, VFreeBusy):
                if c.timespan.intersects(self.get_timespan()):
                    list_of_timestamps_with_parents.append(c.timespan)
            else:
                # There is no way to extend iana-props or x-props for now. If you feel like implementing this, please
                # let me know and open a PR :).
                pass
        return list_of_timestamps_with_parents

    def iterate(self) -> Iterator[Tuple[TimespanWithParent, Component]]:
        """
        Iterate over the `self.__explode_recurring_components()` in chronological order.

        Implementation detail: Using a heap is faster than sorting if the number of events (n) is much bigger than the
        number of events we extract from the iterator (k). Complexity: O(n + k log n).
        """
        heap: List[TimespanWithParent] = self.__explode_recurring_components()
        heapq.heapify(heap)
        while heap:
            popped: TimespanWithParent = heapq.heappop(heap)
            yield popped, popped.parent

    def includes(self, start: DateTime, stop: DateTime) -> Iterator[Component]:
        """Iterate (in chronological order) over every component that is in the specified timespan."""
        query_timespan = Timespan(start, stop)
        for timespan, event in self.iterate():
            if timespan.is_included_in(query_timespan):
                yield event

    def overlapping(self, start: DateTime, stop: DateTime) -> Iterator[Component]:
        """Iterate (in chronological order) over every component that has an intersection with the timespan."""
        query_timespan = Timespan(start, stop)
        for timespan, event in self.iterate():
            if timespan.intersects(query_timespan):
                yield event

    def start_after(self, instant: DateTime) -> Iterator[Component]:
        """Iterate (in chronological order) on every component larger than instant in chronological order."""
        for timespan, event in self.iterate():
            if timespan.begin > instant:
                yield event

    def at(self, instant: DateTime) -> Iterator[Component]:
        """Iterate (in chronological order) over all component that are occurring during `instant`."""
        for timespan, event in self.iterate():
            if timespan.includes(instant):
                yield event

end_date: DateTime property writable

Return the end date of the timeline. No event should start before this value.

start_date: DateTime property writable

Return the start date of the timeline. No event should end before this value.

__explode_recurring_components()

Get a de-duplicated list of all components with a start date, including the recurring components. This means that we add all child component of the :class:VCalendar (except for the :class:VTimeZone instances) to a list and then add all extra occurrences (as recurring components) according to the recurrence properties: :class:RRule, :class:RDate and :class:EXDate.

Returns:

Type Description
List[TimespanWithParent]

A de-duplicated list of all components, including the recurring occurrences of the components.

Source code in ical_library/timeline.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __explode_recurring_components(self) -> List[TimespanWithParent]:
    """
    Get a de-duplicated list of all components with a start date, including the recurring components. This means
    that we add all child component of the :class:`VCalendar` (except for the :class:`VTimeZone` instances) to a
    list and then add all extra occurrences (as recurring components) according to the recurrence properties:
    :class:`RRule`, :class:`RDate` and :class:`EXDate`.
    :return: A de-duplicated list of all components, including the recurring occurrences of the components.
    """
    list_of_timestamps_with_parents: List[TimespanWithParent] = []
    all_children = self.v_calendar.children
    uid_to_datetime_to_exclude = self.__get_items_to_exclude_from_recurrence(all_children)
    for c in all_children:
        # Do some initial filtering.
        if isinstance(c, AbstractComponentWithRecurringProperties):
            if c.max_recurring_timespan.intersects(self.get_timespan()):
                values_to_exclude = uid_to_datetime_to_exclude[c.uid.value]  # .get defaults to None.
                list_of_timestamps_with_parents.extend(
                    c.expand_component_in_range(self.get_timespan(), values_to_exclude)
                )
        elif isinstance(c, VFreeBusy):
            if c.timespan.intersects(self.get_timespan()):
                list_of_timestamps_with_parents.append(c.timespan)
        else:
            # There is no way to extend iana-props or x-props for now. If you feel like implementing this, please
            # let me know and open a PR :).
            pass
    return list_of_timestamps_with_parents

__get_items_to_exclude_from_recurrence(all_components) staticmethod

Deduplicate recurring components. Sometimes it happens that recurring events are changed and this will cause them to both be present as a standard component and in the recurrence.

Parameters:

Name Type Description Default
all_components List[Component]

The list of all component children of the VCalendar instance.

required

Returns:

Type Description
Dict[str, Union[List[Date], List[DateTime]]]

A deduplicated list of components.

Source code in ical_library/timeline.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@staticmethod
def __get_items_to_exclude_from_recurrence(
    all_components: List[Component],
) -> Dict[str, Union[List[Date], List[DateTime]]]:
    """
    Deduplicate recurring components. Sometimes it happens that recurring events are changed and this will cause
    them to both be present as a standard component and in the recurrence.
    :param all_components: The list of all component children of the VCalendar instance.
    :return: A deduplicated list of components.
    """
    start_date_to_timespan_dict: Dict[str, Union[List[Date], List[DateTime]]] = defaultdict(list)
    for component in all_components:
        if isinstance(component, AbstractComponentWithRecurringProperties) and component.recurrence_id is not None:
            start_date_to_timespan_dict[component.uid.value].append(component.recurrence_id.datetime_or_date_value)
    return start_date_to_timespan_dict

at(instant)

Iterate (in chronological order) over all component that are occurring during instant.

Source code in ical_library/timeline.py
146
147
148
149
150
def at(self, instant: DateTime) -> Iterator[Component]:
    """Iterate (in chronological order) over all component that are occurring during `instant`."""
    for timespan, event in self.iterate():
        if timespan.includes(instant):
            yield event

get_timespan()

Return the start and end date as a Timespan.

Source code in ical_library/timeline.py
64
65
66
67
@instance_lru_cache()
def get_timespan(self) -> Timespan:
    """Return the start and end date as a Timespan."""
    return Timespan(self.start_date, self.end_date)

includes(start, stop)

Iterate (in chronological order) over every component that is in the specified timespan.

Source code in ical_library/timeline.py
126
127
128
129
130
131
def includes(self, start: DateTime, stop: DateTime) -> Iterator[Component]:
    """Iterate (in chronological order) over every component that is in the specified timespan."""
    query_timespan = Timespan(start, stop)
    for timespan, event in self.iterate():
        if timespan.is_included_in(query_timespan):
            yield event

iterate()

Iterate over the self.__explode_recurring_components() in chronological order.

Implementation detail: Using a heap is faster than sorting if the number of events (n) is much bigger than the number of events we extract from the iterator (k). Complexity: O(n + k log n).

Source code in ical_library/timeline.py
113
114
115
116
117
118
119
120
121
122
123
124
def iterate(self) -> Iterator[Tuple[TimespanWithParent, Component]]:
    """
    Iterate over the `self.__explode_recurring_components()` in chronological order.

    Implementation detail: Using a heap is faster than sorting if the number of events (n) is much bigger than the
    number of events we extract from the iterator (k). Complexity: O(n + k log n).
    """
    heap: List[TimespanWithParent] = self.__explode_recurring_components()
    heapq.heapify(heap)
    while heap:
        popped: TimespanWithParent = heapq.heappop(heap)
        yield popped, popped.parent

overlapping(start, stop)

Iterate (in chronological order) over every component that has an intersection with the timespan.

Source code in ical_library/timeline.py
133
134
135
136
137
138
def overlapping(self, start: DateTime, stop: DateTime) -> Iterator[Component]:
    """Iterate (in chronological order) over every component that has an intersection with the timespan."""
    query_timespan = Timespan(start, stop)
    for timespan, event in self.iterate():
        if timespan.intersects(query_timespan):
            yield event

start_after(instant)

Iterate (in chronological order) on every component larger than instant in chronological order.

Source code in ical_library/timeline.py
140
141
142
143
144
def start_after(self, instant: DateTime) -> Iterator[Component]:
    """Iterate (in chronological order) on every component larger than instant in chronological order."""
    for timespan, event in self.iterate():
        if timespan.begin > instant:
            yield event