Каков наилучший способ моделирования повторяющихся событий в приложении календаря?


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

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

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

18 201

18 ответов:

Я бы использовал понятие "связь" для всех будущих повторяющихся событий. Они динамически отображаются в календаре и связываются с одним ссылочным объектом. Когда события произошли, связь разрывается, и событие становится автономным экземпляром. Если вы попытаетесь изменить повторяющееся событие, то попросите изменить все будущие элементы (т. е. изменить одну связанную ссылку) или изменить только этот экземпляр (в этом случае преобразуйте его в автономный экземпляр, а затем внесите изменения). Последний случай является немного проблематично, так как вам нужно отслеживать в своем повторяющемся списке все будущие события, которые были преобразованы в один экземпляр. Но, это полностью выполнимо.

Итак, в сущности, есть 2 класса событий-единичные экземпляры и повторяющиеся события.

Martin Fowler-повторяющиеся события для календарей содержит некоторые интересные идеи и шаблоны.

недомерок gem реализует этот шаблон.

там может быть много проблем, связанных с повторяющимися событиями, позвольте мне выделить несколько, которые я знаю.

Решение 1 - нет экземпляров

хранить исходные данные назначения + повторения, не хранить все экземпляры.

проблемы:

  • вам придется рассчитать все экземпляры в окне даты, когда они вам нужны, дорого
  • не удается обрабатывать исключения (т. е. вы удаляете один из экземпляров или перемещаете его, или, скорее, вы не можете сделать это с этим решением)

решение 2-хранить экземпляры

хранить все, начиная с 1, но и все экземпляры, связанные с исходной встречи.

проблемы:

  • занимает много места (но пространство дешево, так незначительно)
  • исключения должны быть обработаны изящно, особенно если вы вернетесь и отредактируете исходную встречу после создания исключения. Например, если вы однажды переместите третий экземпляр вперед, что делать, если вы вернетесь назад и отредактируете время первоначальной встречи, повторно вставьте другой в исходный день и оставьте перемещенный? Разорвать связь с перемещенным? Попробуйте изменить перемещенный соответствующим образом?

конечно, если вы не собираетесь делать исключения, то любое решение должно быть прекрасным, и вы в основном выбираете из сценария времени/пространства.

вы можете посмотреть на программные реализации iCalendar или сам стандарт (RFC 2445RFC 5545). Те, которые приходят на ум быстро являются проекты Mozillahttp://www.mozilla.org/projects/calendar/ быстрый поиск показывает http://icalendar.rubyforge.org/ а также.

другие варианты могут быть рассмотрены в зависимости от того, как вы собираетесь хранить события. Вы создаете свою собственную схему базы данных? Используя что-то iCalendar-based, etc.?

Я работаю со следующим:

и драгоценный камень в процессе, который расширяет formtastic с типом ввода: повторяющийся (form.schedule :as => :recurring), Что делает iCal-подобный интерфейс и before_filter для сериализации представления в IceCube снова возражаю, гетто-ли.

моя идея состоит в том, чтобы сделать его невероятно легко добавить повторяющиеся атрибуты к модели и легко подключить его в представлении. Все в пару строк.


так что же это дает мне? Индексированные, редактируемые, повторяющиеся атрибуты.

events сохраняет экземпляр одного дня и используется в представлении календаря / помощнике скажи task.schedule хранит в YAML бы

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

некоторые из ключевых моментов:

  • рецидив магазине используя iCal rrule format -- это одно колесо вы действительно не хотите изобретать
  • Не хранить отдельные повторяющиеся события экземпляров как строки в вашей базе данных! Всегда храните шаблон повторения.
  • существует много способов разработки схемы событий/исключений, но основной пример начальной точки приведен
  • все значения даты / времени должны храниться в формате UTC и преобразовываться в локальные для отображения
  • дата окончания сохраненный для повторяющегося события всегда должен быть дата окончания диапазона повторения (или "максимальная дата" вашей платформы, если она повторяется "навсегда") и продолжительность события должны храниться отдельно. Это необходимо для обеспечения разумного способа запроса событий позже.
  • некоторые обсуждения вокруг создания экземпляров событий и стратегий редактирования повторения включены

Это действительно сложная тема со многими, многими правильными подходами к ее реализации. Я буду скажем, что я действительно реализовал повторение несколько раз успешно, и я был бы осторожен, принимая советы по этому вопросу от тех, кто на самом деле не сделал этого.

Я использую схему базы данных, как описано ниже, чтобы сохранить параметры повторения

http://github.com/bakineggs/recurring_events_for

затем я использую runt для динамического вычисления дат.

https://github.com/mlipper/runt

  1. следите за правилом повторения (вероятно, на основе iCalendar, per @Крис К.). Это будет включать в себя шаблон и диапазон (каждый третий вторник, для 10 вхождений).
  2. Если вы хотите изменить / удалить конкретное событие, следите за датами исключения для вышеуказанного правила повторения (даты, когда событие не происходят, как указано в правиле).
  3. Если вы удалили, это все, что вам нужно, если вы редактировали, создать другое событие, и дайте ему Родительский идентификатор, установленный для основного события. Вы можете выбрать, включать ли всю информацию о главном событии в эту запись, или если она содержит только изменения и наследует все, что не изменяется.

обратите внимание, что если вы разрешаете правила повторения, которые не заканчиваются, вы должны подумать о том, как отобразить теперь бесконечное количество информации.

надеюсь, что это поможет!

Я бы рекомендовал использовать мощность библиотеки дат и семантику модуля диапазона 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\"]"

из этих ответов я как бы просеял решение. Мне очень нравится идея концепции ссылке. Повторяющиеся события могут быть связанным списком, а хвост знает свое правило повторения. Изменение одного события было бы легко, потому что ссылки остаются на месте, а удаление события также легко - вы просто разблокируете событие, удалите его и повторно свяжите событие до и после него. Вы все еще должны запрашивать повторяющиеся события каждый раз, когда кто-то смотрит на новый период времени, который никогда не рассматривался раньше в календаре, но в остальном это довольно чисто.

вы можете хранить события как повторяющиеся, и если конкретный экземпляр был отредактирован, создайте новое событие с тем же идентификатором события. Затем при поиске события выполните поиск всех событий с одинаковым идентификатором события, чтобы получить всю информацию. Я не уверен, что вы свернули свою собственную библиотеку событий или используете существующую, поэтому это может быть невозможно.

Проверьте статью ниже для трех хороших библиотек даты и времени ruby. ice_cube, в частности, кажется надежным выбором для правил повторения и других вещей, которые понадобятся календарю событий. http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html

в javascript:

обработка повторяющихся расписаний: http://bunkat.github.io/later/

обработка сложных событий и зависимостей между этими графиками: http://bunkat.github.io/schedule/

в принципе, вы создаете правила, а затем просите lib вычислить следующие N повторяющихся событий (указав диапазон дат или нет). Правила могут быть проанализированы / сериализованы для сохранения их в вашей модели.

если вы есть повторяющееся событие и хотел бы изменить только одно повторение вы можете использовать кроме() функция, чтобы закрыть определенный день, а затем добавить новое измененное событие для этой записи.

lib поддерживает очень сложные шаблоны, часовые пояса и даже монотонные события.

хранить события как повторяющиеся и динамически отображать их, однако разрешить повторяющееся событие, чтобы содержать список конкретных событий, которые могут переопределить информацию по умолчанию в определенный день.

при запросе повторяющегося события он может проверить наличие определенного переопределения для этого дня.

Если пользователь вносит изменения, то вы можете спросить, если он хочет обновить для всех экземпляров (детали по умолчанию) или только в тот день (сделать новое конкретное событие и добавить его в список.)

Если пользователь попросит удалить все рецидивы этого события, у вас также есть список особенностей, которые можно легко удалить.

единственный проблемный случай будет, если пользователь хочет обновить это событие и все последующие события. В этом случае вам придется разделить мероприятие на две части. На этом этапе вы можете захотеть связать повторяющиеся события каким-то образом, чтобы вы могли удалить их все.

для программистов .NET, которые готовы платить некоторые лицензионные сборы, вы можете найти Aspose.Сеть полезное... он включает в себя совместимую с iCalendar библиотеку для повторяющихся встреч.

вы храните события в формате iCalendar непосредственно, что позволяет для открытого повторения, локализации часового пояса и так далее.

вы можете хранить их на сервере CalDAV, а затем, когда вы хотите отобразить события, вы можете использовать опцию отчета, определенного в CalDAV, чтобы попросить сервер выполнить расширение повторяющихся событий за рассматриваемый период.

или вы можете хранить их в базе данных самостоятельно и использовать какую-то библиотеку синтаксического анализа iCalendar чтобы сделать расширение, без необходимости PUT / GET/REPORT, чтобы поговорить с серверным сервером CalDAV. Это, наверное, больше работы-я уверен, что серверы CalDAV скрывают сложность где-то.

наличие событий в формате iCalendar, вероятно, упростит ситуацию в долгосрочной перспективе, поскольку люди всегда будут хотеть, чтобы они были экспортированы для размещения другого программного обеспечения в любом случае.

Я просто реализовал эту функцию! Логика заключается в следующем, Сначала вам нужно две таблицы. RuleTable хранить общие или утилизировать отцовские события. ItemTable-это хранимые события цикла. Например, при создании циклического события, время начала для 6 ноября 2015 года, Время окончания для 6 декабря (или навсегда), цикл в течение одной недели. Вы вставляете данные в таблицу правил, поля выглядят следующим образом:

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 (long start, long end), на основе времени начала и окончания, еженедельно, вы можете рассчитать коллекцию, которую вы хотите, . Кроме 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;
}   

Что делать, если у вас есть повторяющиеся встречи без даты окончания? Как бы дешево ни было пространство, у вас нет бесконечного пространства, поэтому решение 2 не является стартером...

могу ли я предположить, что "нет конечной даты" может быть решен до конца дня в конце века. Даже для дневного мероприятия количество места остается дешевым.