241

我正在构建一个需要支持重复事件的组日历应用程序,但是我想出的处理这些事件的所有解决方案似乎都是一个 hack。我可以限制一个人可以看多远,然后一次生成所有事件。或者,我可以将事件存储为重复事件并在日历上向前看时动态显示它们,但如果有人想要更改事件的特定实例的详细信息,我必须将它们转换为正常事件。

我确信有更好的方法可以做到这一点,但我还没有找到。对重复事件建模的最佳方法是什么,您可以在其中更改或删除特定事件实例的详细信息?

(我正在使用 Ruby,但请不要让这限制了你的答案。如果有特定于 Ruby 的库或其他东西,那么很高兴知道。)

4

17 回答 17

104

我会为所有未来的重复事件使用“链接”概念。它们在日历中动态显示并链接回单个参考对象。当事件发生时,链接断开,事件变为独立实例。如果您尝试编辑重复事件,则提示更改所有未来项目(即更改单个链接引用)或仅更改该实例(在这种情况下将其转换为独立实例,然后进行更改)。后一种情况略有问题,因为您需要在循环列表中跟踪所有已转换为单个实例的未来事件。但是,这是完全可行的。

因此,从本质上讲,有 2 类事件 - 单个实例和重复事件。

于 2008-09-17T17:45:23.977 回答
44

我开发了多个基于日历的应用程序,还编写了一组支持循环的可重用 JavaScript 日历组件。我写了一篇关于如何设计重复出现的概述,这可能对某人有帮助。虽然有一些特定于我编写的库的部分,但提供的绝大多数建议对于任何日历实现都是通用的。

一些关键点:

  • 使用iCal RRULE 格式存储重复- 这是你真的不想重新发明的一个轮子
  • 不要将单个重复事件实例存储为数据库中的行!始终存储重复模式。
  • There are many ways to design your event/exception schema, but a basic starting point example is provided
  • All date/time values should be stored in UTC and converted to local for display
  • The end date stored for a recurring event should always be the end date of the recurrence range (or your platform's "max date" if recurring "forever") and the event duration should be stored separately. This is to ensure a sane way of querying for events later. Read the linked article for more details about this.
  • Some discussion around generating event instances and recurrence editing strategies is included

It's a really complicated topic with many, many valid approaches to implementing it. I will say that I've actually implemented recurrence several times successfully, and I would be wary of taking advice on this subject from anyone who hasn't actually done it.

于 2016-07-13T18:25:42.907 回答
34

重复事件可能存在很多问题,让我强调一些我知道的。

解决方案 1 - 没有实例

存储原始约会+重复数据,不存储所有实例。

问题:

  • 您必须在需要时计算日期窗口中的所有实例,成本很高
  • 无法处理异常(即您删除一个实例,或移动它,或者更确切地说,您不能使用此解决方案执行此操作)

解决方案 2 - 存储实例

存储从 1 开始的所有内容,以及链接回原始约会的所有实例。

问题:

  • 占用很多空间(但空间很便宜,所以很小)
  • 必须优雅地处理异常,特别是如果您在异常后返回并编辑原始约会。例如,如果您将第三个实例向前移动一天,如果您返回并编辑原始约会的时间,在原始日期重新插入另一个并离开移动的那个怎么办?取消链接移动的?尝试适当地更改移动的那个?

当然,如果您不打算做例外,那么任何一种解决方案都应该没问题,并且您基本上可以从时间/空间权衡方案中进行选择。

于 2009-05-19T11:38:01.057 回答
20

您可能想查看 iCalendar 软件实现或标准本身 ( RFC 2445 RFC 5545 )。很快就会想到的是 Mozilla 项目http://www.mozilla.org/projects/calendar/ 快速搜索也会发现http://icalendar.rubyforge.org/

根据您将如何存储事件,可以考虑其他选项。您是否正在构建自己的数据库架构?使用基于 iCalendar 的东西等?

于 2008-09-17T17:40:52.880 回答
16

我正在使用以下内容:

还有一个正在进行中的 gem,它使用输入类型 :recurring ( form.schedule :as => :recurring) 扩展了 formtastic,它呈现了一个类似 iCal 的界面和一个before_filter将视图再次序列化为一个IceCube对象,ghetto-ly。

我的想法是让向模型添加重复属性并在视图中轻松连接变得难以置信。全部在几行。


那么这给了我什么?索引,可编辑,重复属性。

events存储一个单日实例,并在日历视图/助手中使用,例如task.schedule存储 yaml'dIceCube对象,因此您可以执行类似 : 的调用task.schedule.next_suggestion

回顾:我使用两种模型,一种用于日历显示,另一种用于功能。

于 2010-10-23T00:19:24.833 回答
7

我正在使用如下所述的数据库模式来存储重复参数

http://github.com/bakineggs/recurring_events_for

然后我使用 runt 来动态计算日期。

https://github.com/mlipper/runt

于 2009-11-09T03:07:33.283 回答
5
  1. 跟踪重复规则(可能基于 iCalendar,每 @ Kris K.)。这将包括一个模式和一个范围(每第三个星期二,出现 10 次)。
  2. 当您想要编辑/删除特定事件时,请跟踪上述重复规则的例外日期(规则指定的事件未发生的日期)。
  3. 如果您删除,这就是您所需要的,如果您编辑,创建另一个事件,并给它一个父 ID 设置为主事件。您可以选择是否在此记录中包含所有主要事件的信息,或者它是否只保存更改并继承所有未更改的内容。

请注意,如果您允许不结束的重复规则,您必须考虑如何显示您现在无限量的信息。

希望有帮助!

于 2008-09-17T17:50:15.040 回答
4

我建议使用日期库的强大功能和 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\"]"
于 2008-09-17T18:01:01.583 回答
3

从这些答案中,我筛选出了一个解决方案。我真的很喜欢链接概念的想法。重复事件可以是一个链表,尾部知道它的重复规则。更改一个事件会很容易,因为链接保持不变,删除一个事件也很容易 - 您只需取消链接一个事件,删除它,然后重新链接它之前和之后的事件。每次有人在日历上查看以前从未查看过的新时间段时,您仍然必须查询重复事件,否则这很干净。

于 2008-09-17T18:04:56.597 回答
2

您可以将事件存储为重复,如果编辑了特定实例,则创建一个具有相同事件 ID 的新事件。然后在查找事件时,搜索具有相同事件ID的所有事件以获取所有信息。我不确定您是否推出了自己的事件库,或者您是否正在使用现有的事件库,所以这可能是不可能的。

于 2008-09-17T17:44:16.897 回答
2

查看下面的文章,了解三个好的 ruby​​ 日期/时间库。对于重复规则和事件日历所需的其他内容,ice_cube 尤其是一个可靠的选择。 http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html

于 2011-02-11T10:31:33.077 回答
1

在 JavaScript 中:

处理重复计划: http ://bunkat.github.io/later/

处理这些时间表之间的复杂事件和依赖关系:http: //bunkat.github.io/schedule/

基本上,您创建规则,然后要求库计算下一个 N 重复事件(指定或不指定日期范围)。可以解析/序列化规则以将它们保存到您的模型中。

如果您有一个重复事件并且只想修改一个重复事件,您可以使用except()函数取消特定的一天,然后为该条目添加一个新的修改事件。

该库支持非常复杂的模式、时区甚至 croning 事件。

于 2014-11-23T10:18:26.843 回答
0

将事件存储为重复并动态显示它们,但是允许重复事件包含特定事件的列表,这些事件可以覆盖特定日期的默认信息。

当您查询重复事件时,它可以检查当天的特定覆盖。

如果用户进行了更改,那么您可以询问他是要更新所有实例(默认详细信息)还是只更新当天(创建新的特定事件并将其添加到列表中)。

如果用户要求删除此事件的所有重复,您还可以获取详细信息列表,并且可以轻松删除它们。

唯一有问题的情况是用户想要更新此事件和所有未来事件。在这种情况下,您必须将重复事件一分为二。此时,您可能需要考虑以某种方式链接重复事件,以便将它们全部删除。

于 2008-09-17T17:47:11.350 回答
0

对于准备支付一些许可费用的 .NET 程序员,您可能会发现Aspose.Network很有用……它包括一个与 iCalendar 兼容的库,用于定期约会。

于 2009-05-19T11:28:11.700 回答
0

您可以直接以 iCalendar 格式存储事件,这允许开放式重复、时区本地化等。

您可以将这些存储在 CalDAV 服务器中,然后当您想要显示事件时,您可以使用 CalDAV 中定义的报告选项来要求服务器在查看的时间段内扩展重复事件。

或者您可以自己将它们存储在数据库中并使用某种 iCalendar 解析库来进行扩展,而无需 PUT/GET/REPORT 与后端 CalDAV 服务器通信。这可能需要更多的工作——我确信 CalDAV 服务器在某处隐藏了复杂性。

从长远来看,拥有 iCalendar 格式的事件可能会使事情变得更简单,因为人们总是希望将它们导出以放入其他软件中。

于 2012-03-14T01:38:51.467 回答
0

我已经简单地实现了这个功能!逻辑如下,首先需要两张表。RuleTable 存储一般或回收父事件。ItemTable 是存储循环事件。例如,当您创建一个循环事件时,开始时间为 2015 年 11 月 6 日,结束时间为 12 月 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.

现在要查询 11 月 20 日到 12 月 20 日的数据。可以写一个函数RecurringEventBE(long start, long end),根据起止时间,WeekLy,可以计算出你想要的集合,<cycleA11.20, cycleA 11.27, cycleA 12.4 ......>。除了 11 月 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:</p>

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;
}   
于 2014-11-06T02:31:12.657 回答
-5

如果您有一个没有结束日期的定期约会怎么办?尽管空间很便宜,但你没有无限的空间,所以解决方案 2 在那里是行不通的......

请问可以将“无结束日期”解决为世纪末的结束日期。即使对于日常活动,空间量仍然很便宜。

于 2010-02-01T14:12:37.647 回答