[轉載]日曆設計之重複事件規則設計

轉載自http://www.cnblogs.com/jcli/p/calendar_recur_rule.html

背景  

  什麼是「日曆」服務,相信你們都用過,或者看到過。就像非計算機時代,你們 也會買個掛曆,而後把何時要作什麼事用筆圈起來,而後每過一個月,一天,就撒一頁,這樣到了作標記處理事情的日子,咱們就能夠知道今天有個什麼事情要 作,好比媽媽的生日,同窗聚會的日子等。固然如今互聯網應用時代咱們會用更好的軟件應用管理好咱們的日曆提醒事件,好比你們最經常使用的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產品功能和 咱們的描述能夠知道想要準確描述一個「重複事件」,要具有以下幾個元素:數據結構

  • 重複類型,你是按天,按周,按月,仍是按年呢
  • 重複頻率,即幾個週期發生一次,如我每兩個月去和朋友打一次球
  • 一個週期內發生的日期,好比你按周重複,那你是每週幾發生呢,是週二,週四才發生,仍是隻週一才發生;若是是按月重複,是每個月的第三個星期六發生,仍是每個月的23號發生呢;
  • 結束日期,即重複多久後終止事件自己。如你每月要還房貸,也是有個最終還完的那天,好比30年後。好比每月參加某個培訓,只參加5次課堂培訓就完了。

如今問題來了,咱們怎樣定義「重複規則」的數據結構呢?基於重複規則的複雜性和彈性可變性(之因此說彈性可變性,由於咱們不能保證本身的產品不會有一些個性化的規則,好比支持農曆日曆,怎樣表示清明節這樣的日期),用字符串表達式定義,持久化存儲更爲理想,就像正則表達式同樣,咱們能夠用一個字符串表達任何豐富的信息在裏面。其實對於重複事件的描述,設計,咱們能夠遵照必定的業界標準。在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 )          
 )
複製代碼

 

上面是「重複規則表達式」的公式定義,詳細解釋以下:框架

  • freq : 事件重複頻率,有效值:DAILY(按天),WEEKLY(按周),MONTHLY(按月),YEARLY(按年)
  • UNTIL: 重複結束日期 格式:20130102T170000Z(2013-1-2 下午5點結束)
  • COUNT: 重複多少次後結束,該字段與UNTIL二者只有出現一次
  • INTERVAL: 事件重複的間隔,如按天重複時,INTERVAL=2,則代表每2天重複一次,默認值 爲1
  • BYDAY: 表示一週的某一天,有效值:MO(週一),TU(週二),WE(週三),TH(週四),FR(週五),SA(週六),SU(週日) , 示例: BYDAY=MO,TH,SU 表示重複日期包括週一,週四,週日. 每一個值前面能夠用 」+」, 「-」 修飾,表示第幾個和倒數第幾個日子,如 BYDAY = 2MO 表示第2個星期一發生; BYDAY=MO,-1SU 表示每一個星期一和最後一個星期日發生
  • BYMONTHDAY: 表示一月的第幾天發生,有效值是 [1 ~ 31] 和 [-31 ~ -1] ,如: BYMONTHDAY=2,18 表示一月的第2天,第18天發生; BYMONTHDAY=-1 表示一月的最後一天
  • BYYEARDAY: 表示一年的第幾天發生,有效值是 [1 ~ 366] 和 [-366 ~ -1], 如 BYYEARDAY=125 表示一年的第125年發生; BYYEARDAY=-1 表示一年的最後一天發生
  • BYWEEKNO: 表示一年的第幾周發生, 有效值是 [1 ~ 53] 和 [-53 ~ -1], 如 BYWEEKNO=2,23 表示一年的第2周,第23周發生
  • BYMONTH: 表示一年中的第幾個月發生, 有效值是 [1 ~ 12]

須要注意幾點:maven

  • 若是各字段所設置的值是無效的,如 BYMONTHDAY=30 ,則會忽略該值
  • 若是某條事件的重複規則表達式缺乏一些必要字段,如 YEARLY;BYMONTH=1 ,表示按年重複,每一年的1月某日發生,如今缺乏」日」字段,則從該事件的」開始日期」中得到

經過上面的公式定義,基本上能夠表示出任何一個重複事件的定義,下面來作一些練習:

按天重複

目標 : 按天重複, 且每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 ,它就是根據重複事件解析後的規則引擎實例接口,它應該具備以下方法:

  • nextOccurDate: 根據傳入的時間計算出以該時間爲起始值的下一次事件發生的時間
  • includes(theDay): 判斷指定時間是不是該事件的發生時間點系列之一

 

 

 上圖中,定義一個按月重複活動(每個月15號發生)。執行nextOccurDate('2013-11-28')方法返回的結果就是以2013-11-28這個時間爲起始值,計算下一次事件發生的時間點,即返回2013-12-15. 其實nextOccurDateincludes兩 個方法表達的意思同樣的,只是從不一樣的兩個方面去定義。咱們知道了任一時間點以後的事件發生時間,那麼咱們也就知道了指定的一個時間是否知足事件發生的要 求。如今咱們要考慮的重點問題是:怎樣簡單高效的實現這兩個方法,保證計算的準確性和性能。在實現以前,咱們有必要來認清這個計算中的難點在哪裏:

  1. 多個週期重複。在按天,周,月,年重複事件中,能夠設置任何的週期倍數,即 每N天/周/月/年 發生一次。
  2. 在按周重複時,一週內能夠設置多天,如每週的星期三,四,五重複
  3. 按月重複時,不僅是簡單的每個月多少號,還有比較複雜的規則:每個月的第幾個星期幾;每個月的最後一天;
  4. 按年重複時,也有每一年的第幾個月的第幾個星期幾重複等複雜規則
  5. 重複事件能夠永不終止,能夠發生到某一天終止,也能夠發生多少次後終止
  6. 在重複事件的時間軸上,能夠去除某幾回的發生時間點,如前面例子中的我不想要2013-10-15號發生,其它時間點不變
  7. ..........

我本身能想到的實現方法有兩種:「實時計算法」和「枚舉法」。下面來分別討論一下:

實時計算法

 實時計算,能夠理解成「無狀態」的實時求值。每次根據傳入的參數計算並返回。

 每次咱們計算時,都沒有任何上下文信息,只要知道開始時間和「重複規則配置」,實時根據公式計算出下一次的發生時間。咱們分析下這個計算過程的可 行性:根據事件最早發生的開始時間和當前傳入的時間值,咱們知道二者的時間差,而後根據「重複週期值:interval」能夠知道下次發生的時間所在的周 期區間。縮小在指定時間週期區間後,再根據具體的某天,某月,某年的信息,便可以算出最終的下次發生的時間點。因此從這裏分析來看,好像理論上是可行的。可是有幾點障礙使我以爲這種計算方法不能很完美:

  1. 這個時間差和Interval的關係在「農曆計算」時我以爲沒有公式計算,主要是閏月的緣由。固然有一種辦法,就是計算兩個農曆時間的差值時,一年一年的判斷累加。可是我以爲這種方法不完美
  2. 「重複次數:Count」這個值基本上實時計算不出來。爲何這麼說呢,由於有些比較特殊的「重複規則」會致使忽略一些時間點。若是每個月31號重複時間,在小月的時候就不會發生。還有每個月的第四個星期三,有時候一個月常常沒有第四個星期幾發生。

枚舉法

 枚舉法,能夠理解成「有狀態」的比較計算。每次調用都是根據傳入的值和「預存計算好的值」比較。

咱們老是先把該重複事件全部要發生的時間線上的點都計算出來,並保存起來。之後每次調用計算方法時,只要根據傳入的參數值立刻知道它的上次和下次發 生時間點。相比上面的「實時計算法」,它的優勢顯而易見:簡單,快速,而且能夠解決上面方法中沒法處理的兩點。可是缺點你也想到了:那要多少空間存儲這些 預計算的值? 可是任何產品,都有它的實際使用場景,我想任何人使用「日曆產品」的時候咱們關注的時間區間都是以今天爲中心兩邊延伸的時間區間,並且通常這個區間不會超 過1年,或者2年吧。因此咱們能夠先計算出以今天爲中心的先後各十年(這個看你估量設置)的時間區間上全部發生時間點。

代碼實現

 

當咱們建立完一個活動事件後(Event),咱們就能夠經過該事件(Event)的「重複規則表達式字符串」,利用 RuleFactory 來建立Rule對象。有了Rule對象,咱們就能夠進行相應的計算求值了。咱們知道 Rule 只是一個接口,咱們返回接口這也符合設計的準則,對外屏蔽內部的具體實現,使調用者根本不用知道里面的計算實現方法。Rule的層級關係以下圖:

這張圖看起來類有點多,可是一點都不復雜,它的層次設計也是徹底按照業務模型來設計的。簡要說明一下這幾個類:

  • Rule 是最頂層接口,用戶直接操做的也只會是這個類型,這樣用戶就不用知道太多細節。
  • AbstractRule 是對事件中和時間相關屬性的一些基本框架方法定義。
  • OnceTimeRule 是一次性事件,即只發生一次非重複發生事件
  • AbstractRecurRule 是全部重複事件的抽象類
  • DailyRule , WeeklyRule , AbstractMonthlyRule , AbstractYearlyRule 分別表明按天,周,月,年重複事件規則。
  • GregorianMonthlyRule,LunarMonthlyRule,GregorianYearlyRule, LunarYearlyRule 分別是農曆,公曆的按月,按年重複事件規則
  • AbstractMutliCalendarRuleHelper,GregorianCalenarRuleHelper,LunarCalenarRuleHelper 是公曆,農曆規則計算中使用到的輔助類。

具體的代碼請參見git項目地址:https://github.com/hongfuli/simplecal ,參考代碼注意幾點:裏面有兩個分支,master和redis這兩個分支對象於「實時計算」和「枚舉」兩種實現方式;代碼沒用maven管理,若是缺乏什麼jar包請上網下載;「枚舉」分支用的redis實現,請了解下redis的使用。

好了,關於規則的設計和討論我就寫到這裏,最後仍是真的但願你們留言把更好的設計告訴我,一塊兒參與討論下。後面文章還會寫關於掃描提醒方面的東西。

 

 

原創文章,轉載請註明出處,謝謝!

相關文章
相關標籤/搜索