Events, Todos and also the Timespans they represent can be compared using the usual python comparision operators ``<``, ``>``, ``<=``, ``>=``, ``==``, ``!=``. This also means that a list of Events can be sorted by a call to ``sort``. See the following sections for details on how this works in different cases. Equality -------- The methods ``__eq__`` and ``__ne__`` implementing ``==`` and ``!=`` are generated by ``attrs`` based on *all* public attributes of the respective class. For an Event, this also includes things like the automatically generated ``UID`` and the timestamps ``created``, ``last_modified`` and ``dtstamp``, where the latter defaults to ``datetime.now``. As the ``UID``\ s are randomly generated and also even two consecutive calls to ``datetime.now()`` usually yield different results, the same holds for constructing two events in sequence: :: >>> from datetime import datetime >>> datetime.now() == datetime.now() False >>> import ics >>> e1, e2 = ics.Event(), ics.Event() >>> e1 == e2 False >>> e1.uid = e2.uid = "event1" >>> e1.dtstamp = e2.dtstamp = datetime.now() >>> e1 == e2 True Also note that for any list of objects, e.g. the list of alarms of an Event, the order is important… :: >>> from datetime import datetime as dt, timedelta as td >>> e1.alarms.append(ics.DisplayAlarm(trigger=td(days=-1), description="Alarm 1")) >>> e1.alarms.append(ics.DisplayAlarm(trigger=td(hours=-1), description="Alarm 2")) >>> e2.alarms = list(reversed(e1.alarms)) >>> e1 == e2 False >>> e2.alarms = list(e1.alarms) >>> e1 == e2 True …and also the ``extra`` Container with custom ``ContentLine``\ s, which is especially important when parsing ics files that contain unknown properties. :: >>> e1.extra.append(ics.ContentLine("X-PRIORITY", value="HIGH")) >>> e1 == e2 False If you want to know the exact differences between two Events, either convert the events to their ics representation using ``str(e)`` or use the ``attr.asdict`` method to get a dict with all attributes. :: >>> e = ics.Event() >>> e # doctest: +ELLIPSIS Event(extra=Container('VEVENT', []), extra_params={}, timespan=EventTimespan(begin_time=None, end_time=None, duration=None, precision='second'), summary=None, uid='...@....org', description=None, location=None, url=None, status=None, created=None, last_modified=None, dtstamp=datetime.datetime(..., tzinfo=Timezone.from_tzid('UTC')), alarms=[], attach=[], classification=None, transparent=None, organizer=None, geo=None, attendees=[], categories=[]) >>> str(e) '' >>> print(e.serialize()) # doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE BEGIN:VEVENT UID:...@...org DTSTAMP:...T...Z END:VEVENT >>> import attr, pprint >>> pprint.pprint(attr.asdict(e)) # doctest: +ELLIPSIS {'alarms': [], 'attach': [], 'attendees': [], 'categories': [], 'classification': None, 'created': None, 'description': None, 'dtstamp': datetime.datetime(..., tzinfo=Timezone.from_tzid('UTC')), 'extra': {'data': [], 'name': 'VEVENT'}, 'extra_params': {}, 'geo': None, 'last_modified': None, 'location': None, 'organizer': None, 'status': None, 'summary': None, 'timespan': {'begin_time': None, 'duration': None, 'end_time': None, 'precision': 'second'}, 'transparent': None, 'uid': '...@....org', 'url': None} Ordering -------- TL;DR: ``Event``\ s are ordered by their attributes ``begin``, ``end``, and ``summary``, in that exact order. For ``Todo``\ s the order is ``due``, ``begin``, then ``summary``. It doesn’t matter whether ``duration`` is set instead of ``end`` or ``due``, as the effective end / due time will be compared. Instances where an attribute isn’t set will be sorted before instances where the respective attribute is set. Naive ``datetime``\ s (those without a timezone) will be compared in local time. Implementation ~~~~~~~~~~~~~~ The class ``EventTimespan`` used by ``Event`` to represent begin and end times or durations has a method ``cmp_tuple`` returning the respective instance as a tuple ``(begin_time, effective_end_time)``: :: >>> t0 = ics.EventTimespan() >>> t0.cmp_tuple() TimespanTuple(begin=datetime.datetime(1900, 1, 1, 0, 0, tzinfo=tzlocal()), end=datetime.datetime(1900, 1, 1, 0, 0, tzinfo=tzlocal())) >>> t1 = ics.EventTimespan(begin_time=dt(2020, 2, 20, 20, 20)) >>> t1.cmp_tuple() TimespanTuple(begin=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), end=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal())) >>> t2 = ics.EventTimespan(begin_time=dt(2020, 2, 20, 20, 20), end_time=dt(2020, 2, 22, 20, 20)) >>> t2.cmp_tuple() TimespanTuple(begin=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), end=datetime.datetime(2020, 2, 22, 20, 20, tzinfo=tzlocal())) It doesn’t matter whether an end time or a duration was specified for the timespan, as only the effective end time is compared. :: >>> t3 = ics.EventTimespan(begin_time=dt(2020, 2, 20, 20, 20), duration=td(days=2)) >>> t2 < t3 False >>> t3 < t2 False The classes ``Event`` and ``Todo`` build on this methods, by appending their ``summary`` to the returned tuple: :: >>> e11 = ics.Event(timespan=t1) >>> e11.cmp_tuple() (datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), '') >>> e12 = ics.Event(timespan=t1, summary="An Event") >>> e12.cmp_tuple() (datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), 'An Event') We define ``__lt__`` (i.e. lower-than, or ``<``) explicitly for ``Timespan``, ``Event`` and ``Todo`` based on comparing their ``cmp_tuple``\ s component-wise (as is the default for comparing python tuples). Please note that neither ``str`` nor ``datetime`` are comparable as less-than or greater-than ``None``. So string values are replaced by the empty string ``""`` and the ``datetime`` values are replaced by ``datetime.min``. This means that instances having no value for a certain parameter will always be sorted before instances where the parameter is set: :: >>> ics.Event(timespan=t0) < ics.Event(timespan=t1) True >>> ics.Event(timespan=t1) < ics.Event(timespan=t2) True >>> ics.Event(timespan=t2) < ics.Event(timespan=t2, summary="Event Name") True The functions ``__gt__``, ``__le__``, ``__ge__`` all behave similarly by applying the respective operation to the ``cmp_tuples``. Note that for ``Todo``\ s the attribute ``due`` has higher priority than ``begin``: :: >>> x1 = ics.Todo(begin=dt(2020, 2, 20, 20, 20)) >>> x2 = ics.Todo(due=dt(2020, 2, 22, 20, 20)) >>> x3 = ics.Todo(begin=dt(2020, 2, 20, 20, 20), due=dt(2020, 2, 22, 20, 20)) >>> x1 < x2 True >>> x1.begin = dt(2020, 4, 4, 20, 20) >>> x1.begin > x2.due True >>> x1 < x2 # even altough x2 now completely lies before x1 True >>> x2 < x3 True Comparison Caveats ~~~~~~~~~~~~~~~~~~ To understand how comparison of events works and what might go wrong in special cases, one first needs to understand how the “rich comparision” operators (``__lt__`` and the like) are `defined `__: By default, ``__ne__()`` delegates to ``__eq__()`` and inverts the result unless it is ``NotImplemented``. There are no other implied relationships among the comparison operators, for example, the truth of ``(x`__. Additionally, as these tuples only represent a part of the instance, the order is not total and the following caveats need to be considered. The equality part in ``<=`` only holds for the compared tuples, but not all the remaining event attributes, thus ``(x<=y and not x y)`` does also *not* imply ``i == y``. See the end of the next section, where this is shown for two ``Timespans`` that refer to the same timestamps, but in different timezones. Unlike all ordering functions, the equality comparision functions ``__eq__`` and ``__ne__`` are generated by ``attr.s(eq=True, ord=False)`` as defined `here `__: They compare the instances as if they were tuples of their attrs attributes, but only iff the types of both classes are identical! This is similar to defining the operations as follows: :: if other.__class__ is self.__class__: return attrs_to_tuple(self) attrs_to_tuple(other) else: return NotImplemented Note that equality, unlike ordering, thus takes all attributes and also the specific class into account. Comparing ``datetime``\ s with and without timezones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, ``datetime``\ s with timezones and those without timezones (so called naive ``datetimes``) can’t directly be ordered. Furthermore, behaviour of some ``datetime`` depends on the local timezone, so let’s first `assume `__ we are all living in Berlin, Germany and have the corresponding timezone set: :: >>> import os, time >>> os.environ['TZ'] = "Etc/GMT-2" >>> time.tzset() >>> time.tzname ('+02', '+02') We can easily compare ``datetime`` instances that have an explicit timezone specified: :: >>> from dateutil.tz import tzutc, tzlocal, gettz >>> dt_ny = dt(2020, 2, 20, 20, 20, tzinfo=gettz("America/New York")) >>> dt_utc = dt(2020, 2, 20, 20, 20, tzinfo=tzutc()) >>> dt_local = dt(2020, 2, 20, 20, 20, tzinfo=tzlocal()) >>> dt_local.tzinfo.tzname(dt_local), dt_local.tzinfo.utcoffset(dt_local).total_seconds() ('+02', 7200.0) >>> dt_utc < dt_ny True >>> dt_local < dt_utc # this always holds as tzlocal is +2:00 (i.e. European Summer Time) True We can also compare naive instances with naive ones, but we can’t compare naive ones with timezone-aware ones: :: >>> dt_naive = dt(2020, 2, 20, 20, 20) >>> dt_naive < dt_local # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: can't compare offset-naive and offset-aware datetimes While comparision fails in this case, other methods of ``datetime`` treat naive instances as local times. This e.g. holds for ```datetime.timestamp()`` `__, which could also be used for comparing instances: :: >>> (dt_utc.timestamp(), dt_ny.timestamp()) (1582230000.0, 1582248000.0) >>> (dt_local.timestamp(), dt_naive.timestamp()) (1582222800.0, 1582222800.0) This can be become an issue when you e.g. want to iterate all Events of an iCalendar that contains both floating and timezone-aware Events in order of their begin timestamp. Let’s consult RFC 5545 on what to do in this situation: DATE-TIME values of this type are said to be “floating” and are not bound to any time zone in particular. They are used to represent the same hour, minute, and second value regardless of which time zone is currently being observed. For example, an event can be defined that indicates that an individual will be busy from 11:00 AM to 1:00 PM every day, no matter which time zone the person is in. In these cases, a local time can be specified. The recipient of an iCalendar object with a property value consisting of a local time, without any relative time zone information, SHOULD interpret the value as being fixed to whatever time zone the “ATTENDEE” is in at any given moment. This means that two “Attendees”, in different time zones, receiving the same event definition as a floating time, may be participating in the event at different actual times. Floating time SHOULD only be used where that is the reasonable behavior. Thus, clients should default to local time when handling floating events, similar to what other datetime methods do. This is also what ics.py does, handling this in the ``cmp_tuple`` method by always converting naive ``datetime``\ s to local ones: :: >>> e_local, e_floating = ics.Event(begin=dt_local), ics.Event(begin=dt_naive) >>> e_local.begin, e_floating.begin (datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), datetime.datetime(2020, 2, 20, 20, 20)) >>> e_local.begin == e_floating.begin False >>> e_local.timespan.cmp_tuple() TimespanTuple(begin=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), end=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal())) >>> e_floating.timespan.cmp_tuple() TimespanTuple(begin=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal()), end=datetime.datetime(2020, 2, 20, 20, 20, tzinfo=tzlocal())) >>> e_local.timespan.cmp_tuple() == e_floating.timespan.cmp_tuple() True So, one floating Event and one Event with explicit timezones can still be compared, while their begin ``datetime``\ s can’t be directly compared: :: >>> e_local < e_floating False >>> e_local > e_floating False >>> e_local.begin < e_floating.begin # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: can't compare offset-naive and offset-aware datetimes Note that neither being considered less than the other hints at both being ordered equally, but they aren’t exactly equal as ``datetime``\ s with different timezones can’t be equal. :: >>> e_local == e_floating False