在日历应用程序中为循环事件build模的最佳方式是什么?

我正在构build一个需要支持周期性事件的组日历应用程序,但是我提出的处理这些事件的所有解决scheme看起来都像是黑客攻击。 我可以限制一个人看起来多远,然后一次生成所有事件。 或者我可以将事件存储为重复,并在日历上向前看dynamic显示,但如果有人想要更改事件的特定实例的细节,则必须将其转换为正常事件。

我相信有更好的方法来做到这一点,但我还没有find它。 模拟周期性事件的最佳方式是什么?您可以在哪里更改或删除特定事件实例的详细信息?

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

我将对所有未来的经常性事件使用“链接”概念。 它们dynamic显示在日历中,并链接回单个参考对象。 事件发生时,链路断开,事件成为独立实例。 如果你试图编辑一个循环事件,然后提示改变所有将来的项目(即更改单个链接的引用)或更改只是该实例(在这种情况下,将其转换为一个独立的实例,然后进行更改)。 后者容易出现问题,因为您需要跟踪转换为单个实例的所有未来事件的定期清单。 但是,这完全是可行的。

所以,实质上,有两类事件 – 单个事件和重复事件。

马丁福勒 – 日历的重复活动包含一些有趣的见解和模式。

侏儒gem实现这种模式。

经常性事件可能会有很多问题,让我强调一些我知道的事情。

解决scheme1 ​​ – 没有实例

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

问题:

  • 当你需要时,你将不得不在date窗口中计算所有实例,代价高昂
  • 无法处理exception(即,您删除了其中一个实例,或者移动它,或者更确切地说,您无法使用此解决scheme执行此操作)

解决scheme2 – 存储实例

存储从1,但也是所有的实例,链接回到原来的约会。

问题:

  • 占用大量的空间(但空间便宜,如此轻微)
  • 必须优雅地处理例外情况,特别是如果您在发出例外情况后返回并编辑原始约会。 例如,如果你前一天移动第三个实例,如果你回去编辑原来的约会时间,在原来的时间重新插入另一个,离开移动的那个? 取消链接移动的? 试着适当地改变移动的那个?

当然,如果你不打算做例外,那么任何一个解决scheme都应该没问题,而且你基本上是从时间/空间的交易场景中select的。

您可能需要查看iCalendar软件实现或标准本身( RFC 2445 RFC 5545 )。 快速想到的是Mozilla项目http://www.mozilla.org/projects/calendar/快速search还会显示http://icalendar.rubyforge.org/

其他选项可以考虑取决于你将如何存储事件。 你在build立自己的数据库模式吗? 使用一些基于iCalendar的等等?

我正在处理以下内容:

和一个正在进行的gem,通过一个inputtypes来扩展form.schedule :as => :recurring :recurring( form.schedule :as => :recurring ),它呈现一个类似于iCal的接口和一个before_filter ,再次将视图序列化为一个IceCube对象。

我的想法是简单地将循环的属性添加到模型并在视图中轻松地连接。 所有在几行。


那么这是什么给我的? 已编入索引,可编辑,重复出现的属性。

events存储一天的实例,并在日历视图/助手中使用,说task.schedule存储yaml'd IceCube对象,所以你可以做如下调用: task.schedule.next_suggestion

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

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

http://github.com/bakineggs/recurring_events_for

然后我使用runt来dynamic计算date。

https://github.com/mlipper/runt

  1. 跟踪重复规则(可能基于iCalendar,per @ Kris K. )。 这将包括一个模式和一个范围(每三个星期二,10次出现)。
  2. 对于要编辑/删除特定事件的情况,请logging上述重复规则的exceptiondate(根据规则指定的事件发生的date)。
  3. 如果您删除了,那么只需要编辑,创build另一个事件,并为其设置父事件ID。 您可以select是否将所有主要事件的信息包含在此logging中,或者只保留更改并inheritance不变的所有内容。

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

希望有所帮助!

我开发了多个基于日历的应用程序,并且还创作了一组支持循环的可重用JavaScript日历组件。 我写了一篇关于如何devise可能对某人有帮助的复发的概述。 虽然有一些特定于我编写的图书馆的内容,但绝大多数的build议对于任何日历实施都是通用的。

一些关键点:

  • 使用iCal RRULE格式存储重现 – 这是您不想重新创build的一个轮子
  • 不要将单个循环事件实例存储为数据库中的行! 始终存储重复模式。
  • 有很多方法来devise你的事件/例外模式,但是提供了一个基本的起点例子
  • 所有date/时间值应以UTC格式保存并转换为本地显示
  • 为重复发生的事件存储的结束date应始终为重复发生范围结束date (或平台的“最长date”,如果经常性发生,则为“永久”),事件持续时间应单独存储。 这是为了确保以后查询事件的方法。
  • 其中包括有关生成事件实例和重复编辑策略的一些讨论

这是一个非常复杂的话题,有许多有效的方法来实现它。 我会说,我已经成功地多次实施了复发,而且我还是会谨慎的从没有真正做过的人那里就这个问题提出build议。

我build议使用date库的function和Ruby范围模块的语义。 经常性事件实际上是时间,date范围(开始和结束),通常是一周中的某一天。 使用date和范围你可以回答任何问题:

 #!/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\"]" 

从这些答案中,我找出了一个解决scheme。 我真的很喜欢链接概念的想法。 重复发生的事件可能是一个链表,尾部知道它的重复规则。 因此,更改一个事件会很容易,因为链接保持不变,并且删除事件也很容易 – 您只需取消链接事件,删除事件并在事件之前和之后重新链接事件即可。 每当有人看到日历上以前从未看过的新时间段时,您仍然必须查询周期性事件,但除此之外,这是相当干净的。

您可以将事件存储为重复事件,如果编辑了特定实例,请使用相同的事件ID创build新事件。 然后在查找事件时,search具有相同事件ID的所有事件以获取所有信息。 我不知道你是否推出了自己的活动库,或者如果你使用现有的活动库,所以它可能是不可能的。

查看下面的文章,了解三个好的rubydate/时间库。 特别是ice_cube似乎是重现规则和事件日历所需的其他东西的可靠select。 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/

基本上,你创build规则,然后你问lib来计算下N个重复事件(指定date范围或不)。 规则可以被parsing/序列化以将其保存到模型中。

如果您有重复发生的事件,并且只想修改一个重复事件,则可以使用except()函数closures某个特定date,然后为此条目添加一个新的已修改事件。

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

将事件存储为重复并dynamic显示它们,但允许重复事件包含特定事件的列表,以便覆盖特定date的默认信息。

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

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

如果用户要求删除此事件的所有事件,您还可以获得详细清单,并可以轻松地将其删除。

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

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

您可以直接将事件存储为iCalendar格式,从而实现开放式重复,时区本地化等等。

您可以将它们存储在CalDAV服务器中,然后当您想要显示事件时,可以使用CalDAV中定义的报告选项来请求服务器在查看期间内执行循环事件的扩展。

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

使用iCalendar格式的事件可能使事情变得更简单,因为人们总是希望将它们导出为放入其他软件。

我已经简单地实现了这个function! 逻辑如下,首先你需要两张表。 RuleTable存储常规或回收父项事件。 ItemTable存储循环事件。 例如,创build循环事件时,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) { // 重复事件的模板处理,生成虚拟事件(根据date段) 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"); // 相当于事件的开始date Start long bk_billEndDate = (Long) iMap.get("ep_billEndDate"); // 重复事件的截止date End int bk_billRepeatType = (Integer) iMap.get("ep_recurringType"); // recurring Type long startDate = 0; // 进一步精确判断日记起止点,保证了该段时间断获取的数据不未空,减less不必要的处理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); // 设置重复的开始datelong 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", "algorithm耗时"+(ba)); return finalDataList; } 

如果你有一个没有结束date的定期约会? 像空间一样便宜,你没有无限的空间,所以解决scheme2是一个不起眼的地方…

我是否可以build议在本世纪末“没有结束的date”能够解决到结束date? 即使是一天的活动,空间的数量仍然很便宜。

Interesting Posts