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

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

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

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

Ответ 1

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

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

Ответ 3

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

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

Храните оригинальную запись + рекуррентные данные, не сохраняйте все экземпляры.

Проблемы:

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

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

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

Проблемы:

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

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

Ответ 4

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

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

Ответ 5

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

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

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


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

events хранит экземпляр за один день и используется в календарном представлении/помощнике скажем task.schedule хранит объект yaml'd IceCube, поэтому вы можете делать такие вызовы, как: task.schedule.next_suggestion.

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

Ответ 7

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

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

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

Ответ 8

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

Некоторые ключевые моменты:

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

Ответ 13

В javascript:

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

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

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

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

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

Ответ 14

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

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

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

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

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

Ответ 15

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

Ответ 16

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

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

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

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

Ответ 17

Я просто применил эту функцию! Логика такова: сначала вам нужны две таблицы. 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 ноября, а остальное я назвал его виртуальным мероприятием. Когда пользователь изменяет имя виртуального события после (например, циклA11.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 (длинный старт, длинный конец) вы используете эти данные, охватывающие виртуальное событие (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;
}   

Ответ 18

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

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