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

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

What's the best way to model recurring events in a calendar application?

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

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

(我使用的是Ruby,但是请不要让它限制您的答案。但是,如果有特定于Ruby的库之类的东西,那很高兴知道。)


我会在以后的所有重复事件中使用"链接"概念。它们将动态显示在日历中,并链接回单个参考对象。事件发生后,链接断开,事件成为独立实例。如果您尝试编辑重复发生的事件,请提示更改所有将来的项目(即更改单个链接的引用)或仅更改该实例(在这种情况下,将其转换为独立实例,然后进行更改)。后者的情况有点问题,因为您需要跟踪所有转换为单个实例的将来事件的重复列表。但是,这完全是可行的。

因此,从本质上讲,有2类事件-单个实例和重复发生的事件。


Martin Fowler-日历的重复事件包含一些有趣的见解和模式。

矮宝石实现了这种模式。


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

解决方案1-没有实例

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

问题:

  • 需要时,您必须在日期窗口中计算所有实例,这很昂贵
  • 无法处理异常(例如,您删除一个实例,或将其移动,或者,您无法使用此解决方案来执行此操作)

解决方案2-存储实例

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

问题:

  • 占用大量空间(但是空间便宜,所以很小)
  • 必须妥善处理异常,特别是如果您在产生异常后返回并编辑原始约会时。例如,如果您将第三个实例向前移动一天,那么如果您回去编辑原始约会的时间,然后在原始日期重新插入另一个并留下已移动的日期,该怎么办?取消移动的链接?尝试适当地更改已移动的?

当然,如果您不打算做例外,那么任何一种解决方案都应该很好,并且您基本上可以从时间/空间权衡方案中进行选择。


您可能需要查看iCalendar软件实现或标准本身( RFC 2445 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...

    我可以建议将"无结束日期"解析为本世纪末的结束日期。即使对于日常活动,空间量仍然很便宜。


    推荐阅读