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

250
我正在构建一个团队日历应用程序,需要支持重复事件,但我想到的所有处理这些事件的解决方案都似乎是一种权宜之计。我可以限制用户查看未来多远的事件,并一次性生成所有事件。或者将事件存储为重复事件,在日历上向前查看时动态显示它们,但如果有人想更改特定事件实例的详细信息,则必须将其转换为普通事件。
我确信有更好的方法来处理这个问题,但我还没有找到。怎么样才能最好地模拟重复事件,以便您可以更改特定事件实例的详细信息或删除它们?
(我使用Ruby语言编写,但请不要让这限制您的答案。如果有Ruby特定的库或其他内容可用,那么这很重要。)

2
《Martin Fowler - 为日历创建重复事件》一文包含了一些有趣的见解和模式。Runt gem 实现了这种模式。 - user219760
17个回答

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

非常喜欢您将事件链接并在其过去后转换为独立的想法。有两个问题:
  • 为什么要将它们转换为独立的固定实例?为什么不完全保持动态?
  • 您能分享所提出的链接概念的参考资料吗! 提前致谢!
- rtindru
@rtindru 我发现将事件转换为独立的用例是在你必须使用数据库中的其他模型与事件模型时。例如,对于检查事件出席情况,您将希望将用户与实际发生(或将要发生)的事件相关联。 - Clinton Yeboah

62

我开发了多个基于日历的应用程序,并编写了一组可重用的JavaScript日历组件,支持重复。我撰写了一篇有关如何设计重复事件的概述,可能对某些人有帮助。虽然其中有一些内容是特定于我编写的库的,但提供的大部分建议适用于任何日历实现。

以下是一些关键点:

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

这是一个非常复杂的话题,有许多有效的实现方法。我可以说,我已经成功地实现了多次重复,因此我会谨慎地从任何未真正实践过此主题的人那里获得建议。


2
@BrianMoeskau 但是,如果某些事件已经发生后,有人编辑RRULE,那么过去的日历视图是否会显示不准确的信息呢?或者在这种情况下,您会“分支”RRULE,并保留修改后的RRULE模式的版本,以准确表示实际过去的事件? - christian
2
@BrianMoeskau 另外一个问题,关于存储UTC并转换为本地时间进行显示:我同意这是一般最佳实践,但如果您有一个涵盖夏令时更改的重复事件,它不会出现问题吗?我的意思是:如果我们基于UTC进行所有计算,那么“每个星期一上午8点当地时间”(这可能是用户想要的)将变成“星期一上午9点当地夏令时”(对于典型的夏令时更改-或者在夏令时结束后反过来,如果实际上是在夏令时期间创建的条目)。我错过了什么吗? - christian
1
@christian 当你在大多数日历中更新重复规则时,它们通常会提示“编辑所有事件、仅此事件或仅限未来”,让用户选择行为。在大多数情况下,用户可能想要“只更改将来的事件”,但是再次强调,由您决定您的软件如何工作以及向用户提供哪些选项。 - Brian Moeskau
2
@Brian Moeskau 我所在的地方,本地时间是UTC-3。假设我创建一个“每周一8:00本地时间”的事件;它将被存储为“11:00Z”。进入夏令时后,本地时间变成了UTC-2。该事件现在将从数据库中恢复为“11:00Z” == “9:00 UTC-2”(我的夏令时本地时间),这不是我想要的(我想要的是无论是否进入夏令时,都是8:00本地时间)。请注意:如果我在夏令时期间创建了该事件(8:00 UTC-2),则数据库记录将为“10:00Z”,而在非夏令时期间将被恢复为“7:00 UTC-3”(同样不是我想要的)。 - christian
1
@mattbatman 很好的问题 - 在我当时的实现中,我不需要处理到单个实例的 URL 路由,但我可以看出它的用处。我可能会使用事件记录 ID + 实例日期(如果允许小于 "daily" 的重复分辨率,则也要包括时间)。您可以对该值进行编码,但甚至可以像 /event/<id>/<date> 这样以 RESTful 的方式来保持可读性,例如 /event/1234/2021-01-01。这将使您能够轻松地从 UI 渲染代码中生成链接,而无需任何复杂的额外逻辑。 - Brian Moeskau
显示剩余10条评论

35

有许多关于定期事件的问题,让我重点介绍一些我知道的。

解决方案1-无实例

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

问题:

  • 需要在需要时计算日期窗口中的所有实例,代价高昂
  • 无法处理异常(即删除其中一个实例或移动它,或者说,您不能使用此解决方案)

解决方案2-存储实例

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

问题:

  • 占用大量空间(但空间便宜,所以不太重要)
  • 必须优雅地处理异常,特别是如果您在制作异常后返回并编辑原始约会。例如,如果您将第三个实例向前一天移动,如果您返回并更改原始约会的时间,在原始日期重新插入另一个并保留已移动的那个呢?取消链接已移动的那个?尝试适当更改已移动的那个?

当然,如果您不打算进行异常处理,则两种解决方案都应该可以,您基本上可以从时间/空间权衡方案中进行选择。


42
如果你有一个没有结束日期的经常性约会,该怎么办? 尽管空间很便宜,但你并没有无限的空间,所以解决方案2在这里行不通... - Shaul Behr
14
方案 #1 实际上可以处理异常情况。例如,RFC5545 建议将它们存储为:a) 排除日期的列表(当您删除一个事件时);b) "具体化" 的事件实例并引用原型(当您移动一个事件时)。 - Andy Mikhaylenko
2
@Shaul:我认为这不是一个无法实现的想法。John Skeet在SO上很受尊重,他在回答基本相同的问题时建议将生成的实例存储在以下链接中:https://dev59.com/Sm855IYBdhLWcg3wnFxO#10151804 - User

20

7
RFC2445似乎已经被RFC5545取代了(http://tools.ietf.org/html/rfc5545)。 - Eric Freese

16

我正在使用以下内容:

还有一个正在进行中的宝石,可以通过扩展带有输入类型:recurring的formtastic(form.schedule :as => :recurring)来呈现类似iCal的接口,并使用before_filter将视图序列化为IceCube对象。

我的想法是使添加重复属性到模型并在视图中轻松连接变得非常容易。一切都只需要几行代码。


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

events 存储一个单一的日期实例,并用于日历视图/帮手。例如,task.schedule 存储了 yaml 格式的 IceCube 对象,所以您可以执行像 task.schedule.next_suggestion 这样的调用。

总结:我使用了两个模型,一个是扁平的用于日历显示,另一个是有属性的用于功能。


7

5
  1. 跟踪循环规则(可能基于iCalendar,参见@Kris K.)。这将包括一个模式和一个范围(例如:每个月第三个星期二,持续10次)。
  2. 如果您想要编辑/删除特定的事件,需要跟踪上述循环规则的异常日期(即事件不按规则发生的日期)。
  3. 如果您进行了删除操作,则只需删除该事件。如果您进行了编辑操作,请创建另一个事件,并将其父ID设置为主事件。您可以选择在此记录中包含所有主事件的信息,或者仅保留更改并继承所有未更改的内容。

请注意,如果允许无限期的循环规则,则必须考虑如何显示无限数量的信息。

希望这有所帮助!


4

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

2
这不是非常灵活的。经常发生的事件模型通常需要指定重复周期(每小时,每周,每两周等)。此外,重复可能没有总数限制,而是有最后一次发生的结束日期。 - Bo Jeanes
一个重复事件通常只是一周中的某一天,这只是一个有限的用例,并不能处理许多其他情况,比如“每个月的第五天”等。 - theraven

3

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


2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接