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

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

我相信有更好的办法,但我还没找到。对重复发生的事件建模的最佳方法是什么?在这种方法中,您可以更改或删除特定事件实例的细节?

(我使用Ruby,但请不要让这限制了你的回答。如果有ruby特定的库或其他东西,那么最好知道。)

70998 次浏览
你可能想看看iCalendar软件实现或标准本身(RFC 2445 RFC 5545)。 很快想到的是Mozilla项目http://www.mozilla.org/projects/calendar/,快速搜索也会显示http://icalendar.rubyforge.org/

可以考虑其他选项,这取决于您将如何存储事件。您正在构建自己的数据库模式吗?使用基于icalendar的软件,等等?

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

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

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

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

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

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

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

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

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

请注意,如果允许不终止的递归规则,则必须考虑如何显示现在无限多的信息。

希望有帮助!

我建议使用ruby的date库的功能和range模块的语义。一个重复发生的事件实际上是一个时间,一个日期范围(一个开始&结束),通常是一周中的某一天。使用日期&范围你可以回答任何问题:

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

从这些答案中,我已经筛选出了一个解决方案。我真的很喜欢链接这个概念。循环事件可以是一个链表,尾部知道它的循环规则。这样更改一个事件就很容易了,因为链接仍然存在,删除一个事件也很容易——只需断开一个事件的链接,删除它,然后在它之前和之后重新链接事件。每当有人查看日历上从未查看过的新时间段时,您仍然必须查询重复发生的事件,但除此之外,这非常干净。

对于准备支付一些授权费用的。net程序员,你可能会发现Aspose。网络很有用…它包括一个iCalendar兼容库,用于重复约会。

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

解决方案1 -没有实例

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

问题:

  • 当你需要的时候,你必须在一个日期窗口中计算所有的实例,成本很高
  • 无法处理异常(即。你删除一个实例,或者移动它,或者更确切地说,你不能用这个解决方案这样做)

解决方案2 -存储实例

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

问题:

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

当然,如果不打算使用异常,那么任何一种解决方案都可以,基本上可以从时间/空间权衡方案中进行选择。

我使用如下所述的数据库模式来存储递归参数

http://github.com/bakineggs/recurring_events_for

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

https://github.com/mlipper/runt

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

我建议,“没有结束日期”可以解决到本世纪末的结束日期。即使是日常活动,场地数量也很便宜。

我正在处理以下问题:

还有一个正在进行中的gem,它将formtastic扩展为一个输入类型:recurrent (form.schedule :as => :recurring),它呈现一个类似ical的接口和一个before_filter,以逐个地将视图再次序列化为IceCube对象。

我的想法是使它难以置信的容易添加循环属性到一个模型,并在视图中容易地连接它。都在几行字里。


这给了我什么?索引,可编辑,循环属性。

events存储一个单天实例,用于日历视图/helper 比如task.schedule存储了yaml的IceCube对象,所以你可以这样调用:task.schedule.next_suggestion.

概述:我使用两个模型,一个平面,日历显示,和一个属性的功能。

查看下面的文章,找到三个好的ruby日期/时间库。 对于递归规则和事件日历需要的其他东西,Ice_cube似乎是一个可靠的选择。 http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html < / p >

您可以直接将事件存储在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)

这是我的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;
}

在javascript中:

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

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

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

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

我开发了多个基于日历的应用程序,还编写了一组支持递归的可重用JavaScript日历组件。我写了一个如何设计递归的概述,可能对别人有帮助。虽然有一些建议是针对我所编写的库的,但所提供的绝大多数建议都适用于任何日历实现。

以下是一些要点:

  • 使用iCal RRULE格式存储递归——这是一个你真的不想重新发明的轮子
  • 不要将单个重复事件实例作为行存储在数据库中!始终存储递归模式。
  • 有许多方法可以设计事件/异常模式,但这里提供了一个基本的起点示例
  • 所有日期/时间值都应以UTC格式存储,并转换为本地格式以供显示
  • 为重复事件存储的结束日期应该始终是复发范围的结束日期(或您的平台的“max date"如果反复出现,“永远”)和事件持续时间应分开存储。这是为了确保以后以合理的方式查询事件。阅读链接文章了解更多细节。
  • 本文还讨论了如何生成事件实例和重复编辑策略

这是一个非常复杂的话题,有很多很多有效的方法来实现它。我要说的是,我实际上已经成功地实现了几次递归,并且我会谨慎地从那些没有实际使用过递归的人那里获得建议。