什麼是「日曆」服務,相信你們都用過,或者看到過。就像非計算機時代,你們 也會買個掛曆,而後把何時要作什麼事用筆圈起來,而後每過一個月,一天,就撒一頁,這樣到了作標記處理事情的日子,咱們就能夠知道今天有個什麼事情要 作,好比媽媽的生日,同窗聚會的日子等。固然如今互聯網應用時代咱們會用更好的軟件應用管理好咱們的日曆提醒事件,好比你們最經常使用的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的使用。
好了,關於規則的設計和討論我就寫到這裏,最後仍是真的但願你們留言把更好的設計告訴我,一塊兒參與討論下。後面文章還會寫關於掃描提醒方面的東西。