9cdefafd

Created Started Cancelled

By: Tom Sydney Kerckhove <syd@cs-syd.eu>

Reconcile the components of a UID when importing a calendar

A component with a RECURRENCE-ID replaces the instance of its series that
it names rather than adding an occurrence beside it, and Google and
Outlook write one for every "this event only" edit.  Recurring one
component at a time cannot know that, so the edited instance was imported
twice: once where it used to be and once where it now is.

'deduplicateBasedOnId' was the hand-rolled version of this.  It sorted
groups by their descending first occurrence on the theory that the
earliest-starting group is the original, then dropped any occurrence
already seen.  Because the key was the whole occurrence, start and end, it
only worked when the override changed neither, which is the uncommon case:
move the override's DTEND by a minute and the duplicate came back.  It
also deduplicated across UIDs, which nothing justifies.  It is deleted.
ical-recurrence 0.3.0.0 reconciles a UID's components, so the library does
this now, and correctly.

'RecurringEvents' therefore has to carry what reconciliation needs from
each component, so 'RecurringEvent' gains its SEQUENCE and its
RECURRENCE-ID.  There is still no UID field, because the map is keyed by
it; 'recurringOf' puts the two back together for 'ICal.recur'.  Both new
fields are omitted at their defaults, so an old picked file still parses
and means what it used to.

The pipeline keeps its shape of one 'Static' plus a set of start and end
pairs.  What changes is the grouping key: reconciliation can put
occurrences from two different components in one UID's recurrence set, and
each occurrence carries the component it came from, so 'Recur' groups by
that component rather than by UID.  'ResolveZones' and everything below it
are unchanged apart from the renamed types.

'Pick' can no longer drop the components it does not want, so 'Static'
gains an 'Inclusion' and 'Recur' drops the excluded occurrences after
reconciliation instead.  Both of the filters need this:

* STATUS:CANCELLED on a component with a RECURRENCE-ID is how "delete this
  one instance" is written.  Dropping that component before the recurrence
  set is reconciled makes the series' instance reappear at that time
  instead of disappearing, which was a live bug.
* SMOS_NO_CALENDAR_IMPORT is a per-component opt-out, and an overriding
  component has a description of its own.

Goldens, all checked by hand against a separately derived expectation:

* test_resources/recurrence-id.* is new, and is the case that matters: a
  conforming RECURRENCE-ID moving one instance of a weekly series.  Its
  .unresolved shows the series keeping the two instances it still has and
  no longer the one that was overridden.
* uid.ics keeps its malformed RECURRENCE-ID, which is zoned where the
  series' DTSTART is floating (RFC 5545 section 3.8.4.4 forbids that) and
  names a Friday where the series generates Mondays.  It matches no
  instance, so it is now an orphan: the override is kept, the series still
  produces the instance the override failed to name, and
  RecurrenceIdentifierUnmatched says the input needs fixing.  That is one
  more occurrence than before, and the old output was luck rather than
  correctness: 'deduplicateBasedOnId' happened to drop the series' instance
  because the override's start and end were byte-identical to it.
* ignored.recurring is no longer empty, because the component it holds is
  now picked with include: false rather than dropped.  Everything
  downstream of it is still empty.
* museum.recurring and exception.recurring gain a sequence field, because
  those two components have a SEQUENCE other than 0.

Three more move for the ical bump alone rather than for anything here:
museum.parsed and visit.parsed gain the X-WR-TIMEZONE that those calendars
always had and the library now round-trips, and
zoned-recurring.unresolved loses a duplicate instance, which is the
already-fixed bug where DTSTART and the recurrence rule both produced the
first instance with two different ends.
GitHub