Как мы синхронизируем повторяющиеся события
Обычное событие устроено просто: начало, конец, название. С ним трудно ошибиться. Сложности начинаются, когда кто-то пишет в календарь «йога каждый вторник» и через месяц переносит одно занятие на среду.
Как это хранится
В формате iCalendar повторяющееся событие описывается одной записью с правилом RRULE. Правило говорит, например, что событие повторяется каждую неделю по вторникам. Отдельных записей на каждый вторник не существует: их вычисляет программа, которая показывает календарь.
Когда человек меняет одно занятие, появляется исключение. Это отдельная запись, которая ссылается на серию через RECURRENCE-ID и говорит: вместо занятия такого-то числа теперь вот это. Если занятие просто удалено, дата попадает в список EXDATE основной записи.
На бумаге всё аккуратно. На практике у каждого сервиса свой взгляд на детали.
Где всё ломается
Некоторые сервисы отдают серию одной записью вместе с исключениями. Другие по запросу разворачивают её в отдельные события, и связь с серией приходится восстанавливать по косвенным признакам. Третьи поддерживают не все варианты правил: скажем, «последняя пятница месяца» где-то работает, а где-то превращается в «четвёртая пятница», что совсем не одно и то же.
Самый коварный случай выглядит так. Человек сдвигает всю серию на час, а одно занятие в ней уже было перенесено вручную. Что должно произойти с перенесённым занятием? Google в такой ситуации ведёт себя иначе, чем Outlook, а iCloud иногда просто разрывает серию на две.
Как решили мы
Первая версия пыталась разворачивать всё в отдельные события и синхронизировать их по одному. Это просто в реализации и ужасно в результате: серия из года занятий превращалась в пятьдесят две записи, и правка всей серии в другом календаре становилась невозможной.
Сейчас мы храним у себя серию как серию: правило и список исключений. При передаче в другой сервис мы смотрим, что он умеет.
- Если правило поддерживается целиком, передаём его как есть вместе с исключениями.
- Если поддерживается частично, передаём ближайшее точное правило и добавляем отдельными событиями то, что в него не вошло.
- Если не поддерживается совсем, разворачиваем серию на ограниченный срок вперёд и обновляем её по мере приближения дат.
У каждого занятия внутри есть устойчивый идентификатор, составленный из идентификатора серии и исходной даты. Поэтому, когда приходит правка из любого источника, мы точно знаем, к какому занятию она относится, даже если у него уже другое время.
Что осталось неидеальным
Если изменить серию сразу в двух календарях в промежутке между синхронизациями, побеждает последнее изменение целиком, и мы показываем об этом предупреждение. Объединять такие правки автоматически мы пока не решаемся: цена ошибки здесь выше, чем неудобство от одного сообщения.
Ещё одна неприятная область связана с участниками. Если вас пригласили на серию встреч, менять её может только организатор. Приходится различать, какие правки можно передавать обратно, а какие останутся только у вас. Иначе организатор начнёт получать странные обновления, о которых никто не просил.
Если вы встретите серию, которая ведёт себя странно, напишите нам из приложения и приложите название сервиса. Такие случаи для нас самые полезные: почти каждый из них превращается в новый тест.