Я создаю приложение для группового календаря, которое должно поддерживать повторяющиеся события, но все решения, которые я придумал для обработки этих событий, кажутся взломанными. Я могу ограничить, насколько далеко можно заглянуть вперед, а затем сгенерировать все события сразу. Или я могу хранить события как повторяющиеся и динамически отображать их, когда кто-то смотрит вперед в календаре, но мне придется преобразовать их в обычное событие, если кто-то захочет изменить детали в конкретном экземпляре события.
Я уверен, что есть способ лучше, но я его еще не нашел. Как лучше всего моделировать повторяющиеся события, когда вы можете изменять детали или удалять определенные экземпляры событий?
(Я использую Ruby, но, пожалуйста, не позволяйте этому ограничивать ваш ответ. Если есть библиотека, специфичная для Ruby, или что-то в этом роде, это полезно знать.)

Вы можете посмотреть на реализацию программного обеспечения iCalendar или на сам стандарт (RFC 2445RFC 5545).
На ум сразу приходят проекты Mozilla http://www.mozilla.org/projects/calendar/. Быстрый поиск также обнаруживает http://icalendar.rubyforge.org/.
Другие варианты могут быть рассмотрены в зависимости от того, как вы собираетесь хранить события. Вы создаете свою собственную схему базы данных? Используете что-то на основе iCalendar и т. д.?
Похоже, RFC2445 устарел из-за RFC5545 (tools.ietf.org/html/rfc5545)
Вы можете сохранить события как повторяющиеся, и, если конкретный экземпляр был отредактирован, создать новое событие с тем же идентификатором события. Затем при поиске события найдите все события с одинаковым идентификатором, чтобы получить всю информацию. Я не уверен, что вы создали свою собственную библиотеку событий или используете существующую, поэтому это может быть невозможно.
Я однажды воспользовался этим решением. Мне нравится принцип хранения измененного экземпляра как нового разового события, которое знает, кто его мама. Таким образом, вы можете оставить все поля пустыми, кроме тех, которые отличаются для дочернего события. Обратите внимание, что вам понадобится дополнительное поле, в котором будет указано, какой дочерний элемент этой матери вы редактируете.
Я бы использовал концепцию «ссылки» для всех будущих повторяющихся событий. Они динамически отображаются в календаре и ссылаются на один объект ссылки. Когда события произошли, ссылка разрывается, и событие становится отдельным экземпляром. Если вы попытаетесь отредактировать повторяющееся событие, тогда предложите изменить все будущие элементы (т.е. изменить одну связанную ссылку) или изменить только этот экземпляр (в этом случае преобразуйте его в автономный экземпляр, а затем внесите изменения). Последний случай немного проблематичен, поскольку вам нужно отслеживать в своем повторяющемся списке все будущие события, которые были преобразованы в один экземпляр. Но это вполне выполнимо.
Итак, по сути, есть 2 класса событий - единичные экземпляры и повторяющиеся события.
Очень нравится ваша идея связывать и преобразовывать события в автономные после того, как они прошли. Два вопроса: - Зачем вообще преобразовывать их в автономные фиксированные инстансы? Почему бы не оставить их полностью динамичными? - Не могли бы вы поделиться ссылкой на предложенную концепцию ссылки! Заранее спасибо!
@rtindru Я нашел вариант использования для преобразования событий в автономные, когда вам нужно использовать модель мероприятие с другими моделями в вашей базе данных. Например, для проверки посещаемости мероприятия вы захотите связать пользователей с реальным событием, которое произошло (или произойдет).
Сохраняйте события как повторяющиеся и динамически отображайте их, однако разрешите повторяющемуся событию содержать список определенных событий, которые могут переопределить информацию по умолчанию в определенный день.
Когда вы запрашиваете повторяющееся событие, он может проверить конкретное переопределение для этого дня.
Если пользователь вносит изменения, вы можете спросить, хочет ли он выполнить обновление для всех экземпляров (данные по умолчанию) или только в этот день (создать новое конкретное событие и добавить его в список).
Если пользователь просит удалить все повторения этого события, у вас также есть список конкретных деталей, и вы можете легко их удалить.
Единственный проблемный случай, если пользователь захочет обновить это событие и все будущие события. В этом случае вам придется разделить повторяющееся событие на два. На этом этапе вы можете захотеть каким-то образом связать повторяющиеся события, чтобы удалить их все.
Обратите внимание: если вы разрешаете правила повторения, которые не заканчиваются, вы должны подумать о том, как отображать ваш теперь бесконечный объем информации.
Надеюсь, это поможет!
Я бы рекомендовал использовать мощь библиотеки дат и семантику модуля диапазона ruby. Повторяющееся событие - это время, диапазон дат (начало и конец) и, как правило, один день недели. Используя дату и диапазон, вы можете ответить на любой вопрос:
#!/usr/bin/ruby
require 'date'
start_date = Date.parse('2008-01-01')
end_date = Date.parse('2008-04-01')
wday = 5 # friday
(start_date..end_date).select{|d| d.wday == wday}.map{|d| d.to_s}.inspect
Производит все дни мероприятия, включая високосный год!
# =>"[\"2008-01-04\", \"2008-01-11\", \"2008-01-18\", \"2008-01-25\", \"2008-02-01\", \"2008-02-08\", \"2008-02-15\", \"2008-02-22\", \"2008-02-29\", \"2008-03-07\", \"2008-03-14\", \"2008-03-21\", \"2008-03-28\"]"
Это не очень гибко. Модель повторяющихся событий часто требует указания периода повторения (ежечасно, еженедельно, раз в две недели и т. д.). Кроме того, повторение может не квалифицироваться общим числом, а скорее датой окончания последнего вхождения.
«Повторяющееся событие - [..] обычно один день недели», это всего лишь один ограниченный вариант использования, который не обрабатывает многие другие, такие как «5-й день каждого месяца» и т. д.
Из этих ответов я как бы отсеял решение. Мне очень нравится идея концепции ссылки. Повторяющиеся события могут быть связанным списком с хвостом, знающим свое правило повторения. Тогда будет легко изменить одно событие, потому что ссылки останутся на месте, и удалить событие тоже легко - вы просто отсоединяете событие, удаляете его и повторно связываете событие до и после него. Вам по-прежнему нужно запрашивать повторяющиеся события каждый раз, когда кто-то смотрит на новый период времени, который раньше никогда не просматривался в календаре, но в остальном это довольно чисто.
Для .NET-программистов, которые готовы платить некоторые лицензионные сборы, вы можете найти Aspose.Network полезным ... он включает совместимую с iCalendar библиотеку для повторяющихся встреч.
С повторяющимися событиями может быть много проблем, позвольте мне выделить несколько, о которых я знаю.
Сохраняйте исходные данные о встречах и повторениях, не сохраняйте все экземпляры.
Проблемы:
Сохраните все, начиная с 1, но также и все экземпляры, связанные с исходной встречей.
Проблемы:
Конечно, если вы не собираетесь делать исключения, тогда подойдет любое решение, и вы в основном выбираете из сценария компромисса времени / пространства.
Что делать, если у вас повторная встреча без даты окончания? Каким бы дешевым ни было пространство, у вас нет бесконечного пространства, поэтому Решение 2 здесь не подходит ...
Решение №1 действительно может обрабатывать исключения. Например, RFC5545 предлагает хранить их как: a) список исключенных дат (при удалении вхождения); б) «материализованные» экземпляры со ссылками на прототип (при перемещении экземпляра).
@ Энди, некоторые интересные дополнения к ответу Лассе. Я попробую.
@Shaul: Я не думаю, что это не для начала. Джон Скит, которого очень уважают в SO, предлагает хранить сгенерированные экземпляры в своем ответе на тот же вопрос: stackoverflow.com/a/10151804/155268
@User - признал, спасибо. Это так странно - я сделал свой комментарий более 4 лет назад, и с тех пор мне действительно не нужно было заниматься этой проблемой. Буквально вчера я занялся разработкой нового модуля, который включает повторяющиеся встречи, и мне было интересно, как с ними справиться. А потом - сегодня утром я получил ТАК-уведомление о вашем комментарии. Очень жутко! Но спасибо! :-)
@ShaulBehr просто из интереса, какое решение вы выбрали? Хранить все или заданное количество экземпляров?
@Harko Этот вопрос, кажется, обладает силой некромантии. Мой первоначальный комментарий был почти 11 лет назад, а последующий - 6 лет назад. Ответ: не помню. Извините ¯\_(ツ)_/¯
Я использую схему базы данных, как описано ниже, для хранения параметров повторения
http://github.com/bakineggs/recurring_events_for
Затем я использую runt для динамического расчета дат.
What if you have a recurring appointment with no end date? As cheap as space is, you don't have infinite space, so Solution 2 is a non-starter there...
Могу я предположить, что "без даты окончания" можно преобразовать в дату окончания в конце века. Даже для ежедневного мероприятия место остается дешевым.
Как скоро мы забываем уроки y2k ... :)
Предположим, у нас есть 1000 пользователей, у каждого из которых есть пара ежедневных событий. 3 события × 1000 пользователей × 365 дней × (2100-2011 = 89 лет) = 97,5 миллиона записей. Вместо 3000 «планов». Эм ...
Я работаю со следующим:
и незавершенный гем, который расширяет formtastic с помощью типа ввода: повторяющийся (form.schedule :as => :recurring), который отображает интерфейс, подобный iCal, и before_filter, чтобы снова сериализовать представление в объект IceCube, гетто.
Моя идея состоит в том, чтобы упростить добавление повторяющихся атрибутов к модели и легкое соединение ее в представлении. Всего в паре строк.
Так что это мне дает? Индексированные, редактируемые, повторяющиеся атрибуты.
events хранит экземпляр одного дня и используется в представлении / помощнике календаря.
скажем, task.schedule хранит yaml'd объект IceCube, поэтому вы можете выполнять такие вызовы, как: task.schedule.next_suggestion.
Резюме: я использую две модели, одну плоскую для отображения календаря и одну с атрибутами для функциональности.
Мне было бы интересно посмотреть, что вы придумали. У вас есть где-нибудь git / блог / доказательство концепции? Спасибо!
Я тоже работаю над чем-то подобным. Хотел бы увидеть вашу реализацию
Мартин Фаулер - повторяющиеся события для календарей содержит некоторые интересные идеи и закономерности.
Гем Runt реализует этот шаблон.
Было бы неплохо получить несколько лучших примеров кода, кто-нибудь знает проект, который реализовал это?
Ознакомьтесь со статьей ниже, чтобы узнать о трех хороших библиотеках даты / времени Ruby. В частности, ice_cube кажется хорошим выбором для правил повторения и прочего, что может понадобиться календарю событий. http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html
Вы сохраняете события напрямую в формате iCalendar, что позволяет неограниченное повторение, локализацию часовых поясов и т. д.
Вы можете сохранить их на сервере CalDAV, а затем, когда вы захотите отобразить события, вы можете использовать параметр отчета, определенный в CalDAV, чтобы попросить сервер выполнить расширение повторяющихся событий в течение просматриваемого периода.
Или вы можете сами сохранить их в базе данных и использовать какую-то библиотеку синтаксического анализа iCalendar для расширения, без необходимости PUT / GET / REPORT для взаимодействия с внутренним сервером CalDAV. Это, вероятно, больше работы - я уверен, что серверы CalDAV где-то скрывают сложность.
Наличие событий в формате iCalendar, вероятно, упростит задачу в долгосрочной перспективе, поскольку люди всегда будут хотеть, чтобы они экспортировались для установки в другое программное обеспечение в любом случае.
Я просто реализовал эту функцию! Логика следующая, для начала вам понадобятся две таблицы. RuleTable хранит общие или повторно использует отцовские события. ItemTable хранит события цикла. Например, когда вы создаете циклическое событие, время начала для 6 ноября 2015 г., время окончания для 6 декабря (или навсегда), цикл в течение одной недели. Вы вставляете данные в RuleTable, поля следующие:
TableID: 1 Name: cycleA
StartTime: 6 November 2014 (I kept thenumber of milliseconds),
EndTime: 6 November 2015 (if it is repeated forever, and you can keep the value -1)
Cycletype: WeekLy.
Теперь вы хотите запросить данные с 20 ноября по 20 декабря. Вы можете написать функцию RecurringEventBE (длинный старт, длинный конец), основанную на времени начала и окончания, WeekLy, вы можете вычислить желаемую коллекцию, <cycleA11.20, cycleA 11.27, cycleA 12.4 ......>. Помимо 6 ноября и всего остального я назвал его виртуальным мероприятием. Когда пользователь меняет имя виртуального события после (например, cycleA11.27), вы вставляете данные в ItemTable. Поля следующие:
TableID: 1
Name, cycleB
StartTime, 27 November 2014
EndTime,November 6 2015
Cycletype, WeekLy
Foreignkey, 1 (pointingto the table recycle paternal events).
В функции RecurringEventBE (long start, long end) вы используете эти данные, охватывающие виртуальное событие (cycleB11.27). извините за мой английский, я попробовал.
Это мой RecurringEventBE :
public static List<Map<String, Object>> recurringData(Context context,
long start, long end) { // 重复事件的模板处理,生成虚拟事件(根据日期段)
long a = System.currentTimeMillis();
List<Map<String, Object>> finalDataList = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> tDataList = BillsDao.selectTemplateBillRuleByBE(context); //RuleTable,just select recurringEvent
for (Map<String, Object> iMap : tDataList) {
int _id = (Integer) iMap.get("_id");
long bk_billDuedate = (Long) iMap.get("ep_billDueDate"); // 相当于事件的开始日期 Start
long bk_billEndDate = (Long) iMap.get("ep_billEndDate"); // 重复事件的截止日期 End
int bk_billRepeatType = (Integer) iMap.get("ep_recurringType"); // recurring Type
long startDate = 0; // 进一步精确判断日记起止点,保证了该段时间断获取的数据不未空,减少不必要的处理
long endDate = 0;
if (bk_billEndDate == -1) { // 永远重复事件的处理
if (end >= bk_billDuedate) {
endDate = end;
startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空
}
} else {
if (start <= bk_billEndDate && end >= bk_billDuedate) { // 首先判断起止时间是否落在重复区间,表示该段时间有重复事件
endDate = (bk_billEndDate >= end) ? end : bk_billEndDate;
startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空
}
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(bk_billDuedate); // 设置重复的开始日期
long virtualLong = bk_billDuedate; // 虚拟时间,后面根据规则累加计算
List<Map<String, Object>> virtualDataList = new ArrayList<Map<String, Object>>();// 虚拟事件
if (virtualLong == startDate) { // 所要求的时间,小于等于父本时间,说明这个是父事件数据,即第一条父本数据
Map<String, Object> bMap = new HashMap<String, Object>();
bMap.putAll(iMap);
bMap.put("indexflag", 1); // 1表示父本事件
virtualDataList.add(bMap);
}
long before_times = 0; // 计算从要求时间start到重复开始时间的次数,用于定位第一次发生在请求时间段落的时间点
long remainder = -1;
if (bk_billRepeatType == 1) {
before_times = (startDate - bk_billDuedate) / (7 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (7 * DAYMILLIS);
} else if (bk_billRepeatType == 2) {
before_times = (startDate - bk_billDuedate) / (14 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (14 * DAYMILLIS);
} else if (bk_billRepeatType == 3) {
before_times = (startDate - bk_billDuedate) / (28 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (28 * DAYMILLIS);
} else if (bk_billRepeatType == 4) {
before_times = (startDate - bk_billDuedate) / (15 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (15 * DAYMILLIS);
} else if (bk_billRepeatType == 5) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 1);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 1 + 1);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 1);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 6) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 2);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 2 + 2);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 2);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 7) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 3);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 3 + 3);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 3);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 8) {
do {
calendar.add(Calendar.YEAR, 1);
virtualLong = calendar.getTimeInMillis();
} while (virtualLong < startDate);
}
if (remainder == 0 && virtualLong != startDate) { // 当整除的时候,说明当月的第一天也是虚拟事件,判断排除为父本,然后添加。不处理,一个月第一天事件会丢失
before_times = before_times - 1;
}
if (bk_billRepeatType == 1) { // 单独处理天事件,计算出第一次出现在时间段的事件时间
virtualLong = bk_billDuedate + (before_times + 1) * 7
* (DAYMILLIS);
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 2) {
virtualLong = bk_billDuedate + (before_times + 1) * (2 * 7)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 3) {
virtualLong = bk_billDuedate + (before_times + 1) * (4 * 7)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 4) {
virtualLong = bk_billDuedate + (before_times + 1) * (15)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
}
while (startDate <= virtualLong && virtualLong <= endDate) { // 插入虚拟事件
Map<String, Object> bMap = new HashMap<String, Object>();
bMap.putAll(iMap);
bMap.put("ep_billDueDate", virtualLong);
bMap.put("indexflag", 2); // 2表示虚拟事件
virtualDataList.add(bMap);
if (bk_billRepeatType == 1) {
calendar.add(Calendar.DAY_OF_MONTH, 7);
} else if (bk_billRepeatType == 2) {
calendar.add(Calendar.DAY_OF_MONTH, 2 * 7);
} else if (bk_billRepeatType == 3) {
calendar.add(Calendar.DAY_OF_MONTH, 4 * 7);
} else if (bk_billRepeatType == 4) {
calendar.add(Calendar.DAY_OF_MONTH, 15);
} else if (bk_billRepeatType == 5) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
1);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 1
+ 1);
} else {
calendar.add(Calendar.MONTH, 1);
}
}else if (bk_billRepeatType == 6) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
2);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 2
+ 2);
} else {
calendar.add(Calendar.MONTH, 2);
}
}else if (bk_billRepeatType == 7) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
3);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 3
+ 3);
} else {
calendar.add(Calendar.MONTH, 3);
}
} else if (bk_billRepeatType == 8) {
calendar.add(Calendar.YEAR, 1);
}
virtualLong = calendar.getTimeInMillis();
}
finalDataList.addAll(virtualDataList);
}// 遍历模板结束,产生结果为一个父本加若干虚事件的list
/*
* 开始处理重复特例事件特例事件,并且来时合并
*/
List<Map<String, Object>>oDataList = BillsDao.selectBillItemByBE(context, start, end);
Log.v("mtest", "特例结果大小" +oDataList );
List<Map<String, Object>> delectDataListf = new ArrayList<Map<String, Object>>(); // finalDataList要删除的结果
List<Map<String, Object>> delectDataListO = new ArrayList<Map<String, Object>>(); // oDataList要删除的结果
for (Map<String, Object> fMap : finalDataList) { // 遍历虚拟事件
int pbill_id = (Integer) fMap.get("_id");
long pdue_date = (Long) fMap.get("ep_billDueDate");
for (Map<String, Object> oMap : oDataList) {
int cbill_id = (Integer) oMap.get("billItemHasBillRule");
long cdue_date = (Long) oMap.get("ep_billDueDate");
int bk_billsDelete = (Integer) oMap.get("ep_billisDelete");
if (cbill_id == pbill_id) {
if (bk_billsDelete == 2) {// 改变了duedate的特殊事件
long old_due = (Long) oMap.get("ep_billItemDueDateNew");
if (old_due == pdue_date) {
delectDataListf.add(fMap);//该改变事件在时间范围内,保留oMap
}
} else if (bk_billsDelete == 1) {
if (cdue_date == pdue_date) {
delectDataListf.add(fMap);
delectDataListO.add(oMap);
}
} else {
if (cdue_date == pdue_date) {
delectDataListf.add(fMap);
}
}
}
}// 遍历特例事件结束
}// 遍历虚拟事件结束
// Log.v("mtest", "delectDataListf的大小"+delectDataListf.size());
// Log.v("mtest", "delectDataListO的大小"+delectDataListO.size());
finalDataList.removeAll(delectDataListf);
oDataList.removeAll(delectDataListO);
finalDataList.addAll(oDataList);
List<Map<String, Object>> mOrdinaryList = BillsDao.selectOrdinaryBillRuleByBE(context, start, end);
finalDataList.addAll(mOrdinaryList);
// Log.v("mtest", "finalDataList的大小"+finalDataList.size());
long b = System.currentTimeMillis();
Log.v("mtest", "算法耗时"+(b-a));
return finalDataList;
}
В javascript:
Обработка повторяющихся графиков: http://bunkat.github.io/later/
Обработка сложных событий и зависимостей между этими расписаниями: http://bunkat.github.io/schedule/
По сути, вы создаете правила, а затем просите библиотеку вычислить следующие N повторяющихся событий (с указанием диапазона дат или нет). Правила могут быть проанализированы / сериализованы для сохранения их в вашей модели.
Если у вас есть повторяющееся событие и вы хотите изменить только одно повторение, вы можете использовать функцию Кроме(), чтобы отклонить определенный день, а затем добавить новое измененное событие для этой записи.
Библиотека поддерживает очень сложные шаблоны, часовые пояса и даже события хронирования.
Я разработал несколько приложений на основе календаря, а также создал набор повторно используемых компонентов календаря JavaScript, поддерживающих повторение. Я написал обзор как спроектировать для повторения, который может быть кому-то полезен. Хотя есть несколько моментов, относящихся к библиотеке, которую я написал, подавляющее большинство предлагаемых советов являются общими для любой реализации календаря.
Некоторые из ключевых моментов:
Это действительно сложная тема с множеством действенных подходов к ее реализации. Я скажу, что я действительно успешно реализовал повторение несколько раз, и я бы опасался советоваться по этому поводу от тех, кто на самом деле этого не делал.
Возможно, сохраните повторы как события, когда они происходят, чтобы ваша история календаря была точной
@RichardHaven Я бы никогда этого не сделал. Вы всегда должны генерировать экземпляры из шаблонов RRULE последовательно, в прошлом, настоящем или будущем. Не было бы причин делать что-то иное для исторических событий. Ваша логика должна просто оценивать RRULE для любого произвольного диапазона дат и возвращать соответствующие экземпляры событий.
@BrianMoeskau хороший и полезный обзор!
@BrianMoeskau Но тогда не будут ли предыдущие просмотры вашего календаря отображать неточную информацию, когда кто-то редактирует RRULE после того, как некоторые события уже произошли? Или, может быть, в этом случае вы «разветвите» RRULE и сохраните модифицированные версии шаблонов RRULE, представляющие в точности фактические прошлые случаи?
@BrianMoeskau Еще один момент, касающийся хранения в формате UTC и преобразования в локальное для отображения: я согласен, что это общая передовая практика, но разве это не ломается, если у вас есть повторяющееся событие, касающееся изменений летнего времени? Я имею в виду: если мы основываем все расчеты в UTC, то «каждый понедельник в 8 утра по местному времени» (что, вероятно, является тем, что хочет пользователь) станет «понедельник 9 утра по местному летнему времени» (для типичных изменений летнего времени - или наоборот после Переход на летнее время заканчивается, если запись была фактически создана во время перехода на летнее время). Я что-то упустил?
@christian Когда вы обновляете правило повторения в большинстве календарей, они обычно запрашивают «редактировать все события, или только это одно, или только будущее», позволяя пользователю выбирать поведение. В большинстве случаев пользователь, вероятно, имеет в виду «изменить его в будущем», но, опять же, вам решать, как работает ваше программное обеспечение и какие параметры вы предоставляете пользователю.
@christian Не уверен, что понимаю вашу точку зрения о летнем времени. Сохранение в формате UTC - единственный реальный вариант, и предполагается, что при преобразовании в местное время ваша логика / библиотека правильно поддерживает DST и использует часовой пояс пользователя. В этом случае он работает, как ожидалось (по моему опыту). Если вы сохранили событие по местному времени пользователя как 1:15 во время перехода на летнее время в октябре, это было до или после изменения времени? Там стабильно будет работать только UTC.
@Brian Moeskau Где я живу, местное время - UTC-3. Допустим, я создаю мероприятие «каждый понедельник в 8:00 по местному времени»; он будет сохранен как «11: 00Z». Переходите на летнее время, местное время меняется на UTC-2. Теперь событие будет восстановлено из базы данных как «11: 00Z» == «9:00 UTC-2» (мое летнее время по местному времени), что не то, что я хотел (я хотел 8:00 по местному времени, независимо от летнего времени. ). Примечание: если я создал событие во время летнего времени (8:00 UTC-2), запись в базе данных будет «10: 00Z», которая после перехода на летнее время будет восстановлена как «7:00 UTC-3» (опять же, не то, что Я хотел).
Возможно, лучшим решением было бы сохранить его в местном времени с исходным часовым поясом (например, сохранить «8:00 UTC-3», что фактически то же самое, что «11: 00Z», но также фиксирует местное время пользователя, когда было создано событие). Это позволит вам получить событие по местному времени пользователя даже с изменением летнего времени, а также позволит корректно преобразовывать часовые пояса других пользователей, если это необходимо.
Чтение эта ссылка напомнило мне обсуждение в последних комментариях. Хотя речь идет конкретно о типах timestamp и timestamptz в PostgreSQL, объяснение различий в концепциях, лежащих в основе этих типов, очень актуально для того, что мы здесь обсуждали.
@BrianMoeskau Я нашел вашу суть / статью очень полезной. Просто любопытно, как бы вы справились с маршрутизацией к одному экземпляру повторяющегося события? Что-то вроде /events/:start_date? Или /events/:encoded_string_including_start_date_signed_with_some_key?
@mattbatman Отличный вопрос - в моих реализациях в то время мне не приходилось обрабатывать маршрутизацию URL-адресов для отдельных экземпляров, но я вижу в этом пользу. Я бы, вероятно, использовал комбинацию из идентификатора записи события + даты экземпляра (плюс время, если разрешено разрешение повторения меньше, чем «ежедневное»). Вы можете закодировать это значение, но вы даже можете сделать это в режиме REST, например / event / <id> / <date>, чтобы оно оставалось читаемым, например / событие / 1234 / 2021-01-01. Это упростило бы создание ссылок из кода рендеринга пользовательского интерфейса без какой-либо сложной дополнительной логики.
если бы вы могли просто дать ссылку на один из них, ваш пост был бы идеальным