首页 文章

在日历应用程序中为重复事件建模的最佳方法是什么?

提问于
浏览
203

我正在构建一个需要支持重复事件的组日历应用程序,但我为处理这些事件而提出的所有解决方案看起来都像是一个黑客攻击 . 我可以限制前方可以看到多远,然后立即生成所有事件 . 或者我可以将事件存储为重复,并在日历上向前看时动态显示它们,但如果有人想要更改特定事件实例的详细信息,我将不得不将它们转换为正常事件 .

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

(我正在使用Ruby,但请不要让它限制你的答案 . 如果有一个特定于Ruby的库或其他东西,那么,这很有用 . )

18 回答

  • 4

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

    因此,实质上,有两类事件 - 单个实例和重复事件 .

  • 11

    Martin Fowler - Recurring Events for Calendars包含一些有趣的见解和模式 .

    Runt gem实现了这种模式 .

  • 79

    重复发生的事件可能会有很多问题,让我重点介绍一下我所知道的事件 .

    解决方案1 - 没有实例

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

    问题:

    • 您需要在需要时计算日期窗口中的所有实例,成本高昂

    • 无法处理异常(即您删除其中一个实例,或移动它们,或者更确切地说,您无法使用此解决方案执行此操作)

    解决方案2 - 存储实例

    存储1中的所有内容,但也存储所有实例,链接回原始约会 .

    问题:

    • 占用了大量空间(但空间很便宜,很小)

    • 必须优雅地处理异常,特别是如果您在发出异常后返回并编辑原始约会 . 例如,如果您向前移动第三个实例,如果您返回并编辑原始约会的时间,在原始日期重新插入另一个并离开移动的约会,该怎么办?取消移动的移动链接?尝试适当更改移动的?

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

  • 2

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

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

  • 31

    我正在使用以下内容:

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

    我的想法是让复杂的属性很容易添加到模型中,并在视图中轻松连接 . 一切都在几行 .


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

    events 存储单日实例,并在日历视图/帮助器中使用,例如 task.schedule 存储yaml'd IceCube 对象,因此您可以执行以下调用: task.schedule.next_suggestion .

    回顾:我使用两个模型,一个平面,用于日历显示,一个属性用于功能 .

  • 0

    我开发了多个基于日历的应用程序,还编写了一组支持重复发生的可重用JavaScript日历组件 . 我写了一篇可能对某人有帮助的how to design for recurrence的概述 . 虽然有一些特定于我编写的库的位,但提供的绝大多数建议对于任何日历实现都是通用的 .

    一些关键点:

    • 使用iCal RRULE format存储重复 - 's one wheel you really don'想要重新发明

    • 不要将单个定期事件 instances 存储为数据库中的行!始终存储重复发生模式 .

    • 有许多方法可以设计事件/异常模式,但提供了一个基本的起点示例

    • 所有日期/时间值都应以UTC格式存储转换为本地显示

    • 为周期性事件存储的结束日期应始终为重复范围的结束日期(或者平台的"max date",如果重复出现"forever"),并且事件持续时间应单独存储 . 这是为了确保以后查询事件的理智方式 .

    • 包括有关生成事件实例和重复编辑策略的一些讨论

    这是一个非常复杂的主题,有很多很多有效的方法来实现它 . 我会说我实际上已经多次成功地实施了复发,而且我会对那些没有真正做过这个问题的人提出建议 .

  • 15

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

    http://github.com/bakineggs/recurring_events_for

    然后我用runt动态计算日期 .

    https://github.com/mlipper/runt

  • 16
    • 跟踪重复规则(可能基于iCalendar,每个@ Kris K.) . 这将包括一个模式和一个范围(每三个星期二,10次出现) .

    • 对于要编辑/删除特定事件的情况,请跟踪上述重复规则的异常日期(事件未按规则指定的情况发生的日期) .

    • 如果您删除了此记录中的's all you need, if you edited, create another event, and give it a parent ID set to the main event. You can choose whether to include all of the main event'信息,或者它只保留更改并继承了不会更改的所有内容 .

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

    希望有所帮助!

  • 2

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

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

  • 51

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

  • 5

    查看下面的文章,了解三个好的红宝石日期/时间库 . ice_cube特别适用于重复规则以及事件日历需要的其他内容 . http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html

  • -5

    在javascript中:

    处理经常性时间表:http://bunkat.github.io/later/

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

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

    If you have a recurring event and would like to modify only one recurrence you can use the except() function to dismiss a particular day and then add a new modified event for this entry.

    lib支持非常复杂的模式,时区甚至是croning事件 .

  • 0

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

    查询周期性事件时,它可以检查当天的特定覆盖 .

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

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

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

  • 1

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

  • 3

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

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

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

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

  • 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:

    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;
    }
    
  • 0

    如果您没有结束日期的定期约会怎么办?像空间一样便宜,你没有无限的空间,所以解决方案2在那里是一个非首发......

    我是否可以建议将“无结束日期”解决为本世纪末的结束日期 . 即使是每日活动,空间的数量仍然很便宜 .

相关问题