我正在构建一个需要支持重复事件的组日历应用程序,但是我为处理这些事件而想出的所有解决方案都像是黑客。 我可以限制一个人可以看到的距离,然后立即生成所有事件。 或者,我可以将事件存储为重复事件,并在日历上向前看时动态显示它们,但是如果有人想更改事件特定实例的详细信息,则必须将它们转换为正常事件。
我敢肯定有更好的方法可以做到这一点,但我还没有找到。 对重复事件进行建模的最佳方法是什么,您可以在其中更改或删除特定事件实例的详细信息?
(我使用的是Ruby,但是请不要让它限制您的答案。但是,如果有特定于Ruby的库之类的东西,那很高兴知道。)
我会在以后的所有重复事件中使用"链接"概念。它们将动态显示在日历中,并链接回单个参考对象。事件发生后,链接断开,事件成为独立实例。如果您尝试编辑重复发生的事件,请提示更改所有将来的项目(即更改单个链接的引用)或仅更改该实例(在这种情况下,将其转换为独立实例,然后进行更改)。后者的情况有点问题,因为您需要跟踪所有转换为单个实例的将来事件的重复列表。但是,这完全是可行的。
因此,从本质上讲,有2类事件-单个实例和重复发生的事件。
Martin Fowler-日历的重复事件包含一些有趣的见解和模式。
矮宝石实现了这种模式。
重复发生的事件可能会有很多问题,让我强调一些我所知道的问题。
解决方案1-没有实例
存储原始约会和重复数据,而不存储所有实例。
问题:
-
需要时,您必须在日期窗口中计算所有实例,这很昂贵
-
无法处理异常(例如,您删除一个实例,或将其移动,或者,您无法使用此解决方案来执行此操作)
解决方案2-存储实例
存储从1开始的所有内容,还存储所有实例,链接回原始约会。
问题:
-
占用大量空间(但是空间便宜,所以很小)
-
必须妥善处理异常,特别是如果您在产生异常后返回并编辑原始约会时。例如,如果您将第三个实例向前移动一天,那么如果您回去编辑原始约会的时间,然后在原始日期重新插入另一个并留下已移动的日期,该怎么办?取消移动的链接?尝试适当地更改已移动的?
当然,如果您不打算做例外,那么任何一种解决方案都应该很好,并且您基本上可以从时间/空间权衡方案中进行选择。
您可能需要查看iCalendar软件实现或标准本身( RFC 2445 strike> RFC 5545)。
很快就会想到的是Mozilla项目http://www.mozilla.org/projects/calendar/快速搜索也显示了http://icalendar.rubyforge.org/。
可以考虑其他选项,具体取决于您将如何存储事件。您正在构建自己的数据库架构吗?使用基于iCalendar的工具等?
我正在处理以下内容:
-
http://github.com/elevation/event_calendar-日历的模型和助手
-
http://github.com/seejohnrun/ice_cube-很棒的重复宝石
-
http://github.com/justinfrench/formtastic-简单表格
以及正在进行的gem扩展形式,其输入类型为: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
然后,我使用矮子来动态计算日期。
https://github.com/mlipper/runt
跟踪重复规则(可能基于iCalendar,基于@Kris K.)。这将包括模式和范围(每第三个星期二,出现10次)。
对于要编辑/删除特定事件的时间,请跟踪上述重复规则的异常日期(规则未指定事件发生的日期)。
如果删除了,那就足够了,如果您进行编辑,则可以创建另一个事件,并将其父ID设置为主要事件。您可以选择是在该记录中包括所有主要事件的信息,还是仅保留更改并继承所有不变的信息。
请注意,如果您允许不终止的重复规则,则必须考虑如何显示现在无限量的信息。
希望有帮助!
我建议使用日期库的功能以及ruby的range模块的语义。重复发生的事件实际上是一个时间,一个日期范围(开始和结束),通常是一周中的一天。使用日期和范围,您可以回答任何问题:
1 2 3 4 5 6 7 8
| #!/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 |
产生事件的所有日子,包括the年!
1
| # =>"["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"]" |
从这些答案中,我找到了一个解决方案。我真的很喜欢链接概念的想法。重复发生的事件可能是一个链表,而尾部知道其重复发生的规则。这样,更改一个事件将很容易,因为链接保持不变,并且删除事件也很容易-您只需取消链接事件,删除它并在事件之前和之后将其重新链接即可。每当有人查看日历上从未见过的新时间段时,您仍然必须查询重复发生的事件,但是这样做很干净。
查看下面的文章,了解三个良好的红宝石日期/时间库。
对于事件规则需要的重复规则和其他内容,ice_cube似乎是一个不错的选择。
http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html
您可以将事件存储为重复事件,如果已编辑特定实例,则使用相同的事件ID创建一个新事件。然后,在查找事件时,搜索具有相同事件ID的所有事件以获取所有信息。我不确定您是否滚动了自己的事件库,或者是否正在使用现有的事件库,因此可能无法实现。
在javascript中:
处理定期计划:
http://bunkat.github.io/later/
处理这些计划之间的复杂事件和依存关系:
http://bunkat.github.io/schedule/
基本上,先创建规则,然后要求lib计算下N个重复发生的事件(是否指定??日期范围)。可以对规则进行解析/序列化,以将其保存到模型中。
如果您有一个重复发生的事件,并且只想修改一个重复发生,则可以使用exception()函数关闭某一天,然后为该条目添加一个新的修改后的事件。
lib支持非常复杂的模式,时区甚至croning事件。
您可以将事件直接以iCalendar格式存储,从而可以进行开放式重复,时区本地化等等。
您可以将它们存储在CalDAV服务器中,然后在要显示事件时,可以使用CalDAV中定义的报告选项来要求服务器在查看期间扩展重复发生的事件。
或者,您也可以自己将它们存储在数据库中,并使用某种iCalendar解析库进行扩展,而无需PUT / GET / REPORT与后端CalDAV服务器对话。这可能是更多的工作-我确信CalDAV服务器会将复杂性隐藏在某个地方。
从长远来看,采用iCalendar格式的事件可能会使事情变得更简单,因为人们总是希望将其导出以放入其他软件中。
我已经实现了此功能!逻辑如下,首先需要两个表。 RuleTable存储常规事件或回收父事件。 ItemTable是存储周期事件。例如,当您创建一个循环事件时,开始时间为2015年11月6日,结束时间为12月6日(或永远),则周期为一周。您将数据插入RuleTable中,字段如下:
1 2 3 4
| 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日的数据。您可以根据开始时间和结束时间WeekLy编写函数RecurringEventBE(长起点,长终点),可以计算所需的集合。
除了11月6日,其余的我都称他为虚拟活动。当用户之后更改虚拟事件的名称(例如,cycleA11.27)时,可以将数据插入到ItemTable中。字段如下:
1 2 3 4 5 6
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
| 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;
} |
将事件存储为重复事件并动态显示它们,但是允许重复发生的事件包含特定事件的列表,这些事件可能会覆盖特定日期的默认信息。
当您查询重复事件时,它可以检查当天的特定替代。
如果用户进行更改,则可以询问他是否要针对所有实例(默认详细信息)或仅在当天进行更新(进行新的特定事件并将其添加到列表中)。
如果用户要求删除此事件的所有重复发生,那么您还可以获取详细信息列表,并且可以轻松删除它们。
唯一有问题的情况是用户是否要更新此事件以及所有将来的事件。在这种情况下,您必须将重复事件分为两部分。此时,您可能需要考虑以某种方式链接重复事件,以便将其全部删除。
对于准备支付一些许可费的.NET程序员,您可能会发现Aspose.Network有用...它包括一个iCalendar兼容库,用于定期约会。
What if you have a recurring appointment with no end date? As cheap as space is, you don't have infinite space, so Solution 2 is a non-starter there...
我可以建议将"无结束日期"解析为本世纪末的结束日期。即使对于日常活动,空间量仍然很便宜。