什么是「日历」服务,相信你们都用过,或者看到过。就像非计算机时代,你们 也会买个挂历,而后把何时要作什么事用笔圈起来,而后每过一个月,一天,就撒一页,这样到了作标记处理事情的日子,咱们就能够知道今天有个什么事情要 作,好比妈妈的生日,同窗聚会的日子等。固然如今互联网应用时代咱们会用更好的软件应用管理好咱们的日历提醒事件,好比你们最经常使用的Google日 历,QQ日历:html
如上图所示,就是Google的日历产品,我添加了一个每个月7号还贷的事件,这样每月的7号前,好比说6号上午9点,我就会收到一封 Google的邮件,或者手机短信提示我明天要还房贷了,这样我就会当即处理这个事情。如今你们应该对这样相似的产品有个感性的认识了,在生活中也能给我 们不少帮助,这样能够在亲人生日,还贷还款,朋友聚会,商务会议等不少场景中帮咱们记忆事件活动提醒,不用本身天天记一堆的事情,并且还容易忘记。好了, 说完这个产品的背景,如今咱们想一想应该怎样从技术上设计,实现这个产品呢?git
设计实现一个「日历」服务产品,我以为有两个难点,一是「重复事件规则计算」,二是「到点事件准时提醒」。这篇文章主要讲解第一点,「重复规则的设计和实现」,下一篇博客讨论下怎样保证准时到点提醒。github
首先,咱们必须定义什么是「事件」或者称「活动」。所谓事件/活动,在「日历产品」中就是咱们建立的提醒事件自己,如「每个月7号还房贷」就是一个事件/活动。正则表达式
/** * 日历事件/活动. * * * @author tony.li.fly@gmail.com */ public class Event { /** * 事件标题 */ private String title; /** * 事件描述 */ private String description; /** * 事件发生地点 */ private String location; /** * 事件发生的开始时间 */ private Date startDate; /** * 事件发生的结束时间 */ private Date endDate; /** * 日历类型: 1,公历 2,农历 */ private int calendarType; /** * 重复事件规则表达式 */ private String rule;
因此一个事件的基本的几个属性有:标题,描述,地点,事件开始时间,事件结束时间,还有后面要重点讨论的重复规则表达式和日历类型。 redis
看下Google产品中设置重复事件的页面:算法
上图是设置一个重复事件的设置页面,能够看到设置项仍是挺多的,有「重复类型」:按日,周,月,年重复;「重复频率」:每几天,几周....等发生 一次;「重复日期」:按月重复时是「一月中的某天呢」,仍是「一周中的某天呢」;「结束日期」:重复事件到何时结束呢。因此从Google产品功能和 咱们的描述能够知道想要准确描述一个「重复事件」,要具有以下几个元素:数据结构
如今问题来了,咱们怎样定义「重复规则」的数据结构呢?基于重复规则的复杂性和弹性可变性(之因此说弹性可变性,由于咱们不能保证本身的产品不会有一些个性化的规则,好比支持农历日历,怎样表示清明节这样的日期),用字符串表达式定义,持久化存储更为理想,就像正则表达式同样,咱们能够用一个字符串表达任何丰富的信息在里面。其实对于重复事件的描述,设计,咱们能够遵照必定的业界标准。在RFC2445中有详细的定义,这里我精简总结下:app
freq *( ; either UNTIL or COUNT may appear in a 'recur', ; but UNTIL and COUNT MUST NOT occur in the same 'recur' ( ";" "UNTIL" "=" enddate ) / ( ";" "COUNT" "=" 1*DIGIT ) / ; the rest of these keywords are optional, ; but MUST NOT occur more than once ( ";" "INTERVAL" "=" 1*DIGIT ) / ( ";" "BYDAY" "=" bywdaylist ) / ( ";" "BYMONTHDAY" "=" bymodaylist ) / ( ";" "BYYEARDAY" "=" byyrdaylist ) / ( ";" "BYWEEKNO" "=" bywknolist ) / ( ";" "BYMONTH" "=" bymolist ) )
上面是「重复规则表达式」的公式定义,详细解释以下:框架
须要注意几点:maven
经过上面的公式定义,基本上能够表示出任何一个重复事件的定义,下面来作一些练习:
目标 : 按天重复, 且每3天重复一次
表达式: DAILY;INTERVAL=3
目标 : 按天重复, 重复到今年结束
表达式: DAILY;UNTIL=20140101T000000Z
目标 : 按天重复, 重复20次
表达式: DAILY;COUNT=20
目标 : 每周二,周四,周日重复,每隔2周发生一次
表达式: WEEKLY;INTERVAL=2;BYDAY=TU,TH,SU
目标 : 每一年的七月,八月两月最后一天
表达式: YEARLY;BYMONTH=7,8;BYMONTHDAY=-1
目标 : 每一年六月的第三个星期日
表达式: YEARLY;BYMONTH=6;BYDAY=3SU
目标 : 农历每一年的最后一天
表达式: YEARLY;BYMONTH=12;BYMONTHDAY=-1
目标 : 每一年11月的第四个星期四
表达式: YEARLY;BYMONTH=11;BYDAY=4TH
目标 : 每一年5月的第二个星期日
表达式: YEARLY;BYMONTH=5;BYDAY=2SU
什么叫重复规则的解析和计算?解析,就是把上面的重复规则表达式解析成咱们程序内部结构化的对象;计算,就是咱们能知道某个重复事件在未来的哪些时间点上会发生。
上图所示,咱们先把重复表达式字符串解析成程序内部的结构化数据实例,而后计算在整个未来的时间轴上该重复事件在什么时间点发生。
咱们先定义一个接口 Rule ,它就是根据重复事件解析后的规则引擎实例接口,它应该具备以下方法:
上图中,定义一个按月重复活动(每个月15号发生)。执行nextOccurDate('2013-11-28')方法返回的结果就是以2013-11-28这个时间为起始值,计算下一次事件发生的时间点,即返回2013-12-15. 其实nextOccurDate和includes两 个方法表达的意思同样的,只是从不一样的两个方面去定义。咱们知道了任一时间点以后的事件发生时间,那么咱们也就知道了指定的一个时间是否知足事件发生的要 求。如今咱们要考虑的重点问题是:怎样简单高效的实现这两个方法,保证计算的准确性和性能。在实现以前,咱们有必要来认清这个计算中的难点在哪里:
我本身能想到的实现方法有两种:「实时计算法」和「枚举法」。下面来分别讨论一下:
实时计算,能够理解成「无状态」的实时求值。每次根据传入的参数计算并返回。
每次咱们计算时,都没有任何上下文信息,只要知道开始时间和「重复规则配置」,实时根据公式计算出下一次的发生时间。咱们分析下这个计算过程的可 行性:根据事件最早发生的开始时间和当前传入的时间值,咱们知道二者的时间差,而后根据「重复周期值:interval」能够知道下次发生的时间所在的周 期区间。缩小在指定时间周期区间后,再根据具体的某天,某月,某年的信息,便可以算出最终的下次发生的时间点。因此从这里分析来看,好像理论上是可行的。可是有几点障碍使我以为这种计算方法不能很完美:
枚举法,能够理解成「有状态」的比较计算。每次调用都是根据传入的值和「预存计算好的值」比较。
咱们老是先把该重复事件全部要发生的时间线上的点都计算出来,并保存起来。之后每次调用计算方法时,只要根据传入的参数值立刻知道它的上次和下次发 生时间点。相比上面的「实时计算法」,它的优势显而易见:简单,快速,而且能够解决上面方法中没法处理的两点。可是缺点你也想到了:那要多少空间存储这些 预计算的值? 可是任何产品,都有它的实际使用场景,我想任何人使用「日历产品」的时候咱们关注的时间区间都是以今天为中心两边延伸的时间区间,并且通常这个区间不会超 过1年,或者2年吧。因此咱们能够先计算出以今天为中心的先后各十年(这个看你估量设置)的时间区间上全部发生时间点。
当咱们建立完一个活动事件后(Event),咱们就能够经过该事件(Event)的「重复规则表达式字符串」,利用 RuleFactory 来建立Rule对象。有了Rule对象,咱们就能够进行相应的计算求值了。咱们知道 Rule 只是一个接口,咱们返回接口这也符合设计的准则,对外屏蔽内部的具体实现,使调用者根本不用知道里面的计算实现方法。Rule的层级关系以下图:
这张图看起来类有点多,可是一点都不复杂,它的层次设计也是彻底按照业务模型来设计的。简要说明一下这几个类:
具体的代码请参见git项目地址:https://github.com/hongfuli/simplecal ,参考代码注意几点:里面有两个分支,master和redis这两个分支对象于「实时计算」和「枚举」两种实现方式;代码没用maven管理,若是缺乏什么jar包请上网下载;「枚举」分支用的redis实现,请了解下redis的使用。
好了,关于规则的设计和讨论我就写到这里,最后仍是真的但愿你们留言把更好的设计告诉我,一块儿参与讨论下。后面文章还会写关于扫描提醒方面的东西。