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

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

(我正在使用 Ruby,但请不要让它限制您的答案。不过,如果有特定于 Ruby 的库或其他东西,那么了解一下就好了。)

有帮助吗?

解决方案

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

因此,本质上有两类事件——单一实例和重复事件。

其他提示

Martin Fowler - 日历的重复事件 包含一些有趣的见解和模式。

矮子 gem 实现了这种模式。

重复发生的事件可能会带来很多问题,让我重点介绍一些我所知道的问题。

解决方案 1 - 无实例

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

问题:

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

解决方案 2 - 存储实例

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

问题:

  • 占用大量空间(但空间很便宜,所以很小)
  • 必须妥善处理例外情况,尤其是当您在例外情况后返回并编辑原始约会时。例如,如果您将第三个实例向前移动一天,如果您返回并编辑原始约会的时间,在原始日期重新插入另一个实例并保留移动的约会,会怎么样?取消已移动的链接?尝试适当改变移动的那个?

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

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

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

我正在处理以下内容:

以及正在开发中的 gem,它使用输入类型扩展了 formattastic :recurring (form.schedule :as => :recurring),它呈现类似 iCal 的界面和 before_filter 将视图序列化为 IceCube 再次反对,贫民窟。

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


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

events 存储一个一天的实例,并在日历视图/帮手中使用 task.schedule 存储 yaml'd IceCube 对象,这样你就可以进行如下调用: task.schedule.next_suggestion.

回顾:我使用两种模型,一种是平面模型,用于日历显示,另一种是用于功能性的属性模型。

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

一些要点:

  • 使用存储重复 iCal RRULE 格式 ——那是你真的不想重新发明的一个轮子
  • 不要存储单个重复事件 实例 作为数据库中的行!始终存储重复模式。
  • 设计事件/异常模式的方法有很多,但提供了一个基本的起点示例
  • 所有日期/时间值应以 UTC 存储并转换为本地显示
  • 为重复事件存储的结束日期应始终是 重复范围的结束日期 (或者您平台的“最大日期”,如果“永远”重复出现)和事件持续时间应单独存储。这是为了确保以后以合理的方式查询事件。
  • 包括一些关于生成事件实例和重复编辑策略的讨论

这是一个非常复杂的主题,有很多很多有效的方法来实现它。我想说的是,我实际上已经成功地实施了多次重复,并且我会谨慎地听取任何尚未实际实施过的人关于这个问题的建议。

我使用如下所述的数据库架构来存储重复参数

http://github.com/bakineggs/recurring_events_for

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

https://github.com/mlipper/runt

  1. 跟踪重复规则(可能基于 iCalendar,根据 @克里斯·K.)。这将包括一个模式和一个范围(每个第三个星期二,出现 10 次)。
  2. 当您想要编辑/删除特定事件时,请跟踪上述重复规则的例外日期(事件发生的日期) 按照规则指定发生)。
  3. 如果您删除了,这就是您所需要的,如果您进行了编辑,则创建另一个事件,并为其指定一个设置为主事件的父 ID。您可以选择是否在此记录中包含所有主要事件的信息,或者仅保留更改并继承所有未更改的内容。

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

希望有帮助!

我建议使用日期库的强大功能和 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\"]"

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

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

查看下面的文章,了解三个优秀的 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/

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

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

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

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

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

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

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

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

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

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

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

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

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

我已经简单地实现了这个功能!逻辑如下,首先需要两张表。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(长开始,长结束),根据起止时间,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(长启动,远端)中,您使用此涵盖虚拟事件的数据(Cycleb11.27)对我的英语感到抱歉,我尝试了。

这是我的重复事件BE:

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 是行不通的......

我是否可以建议“没有结束日期”可以解决为本世纪末结束日期。即使是日常活动,空间仍然很便宜。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top