itok's Lab

昔の開発ネタを記録として残してます

iCalとiCalendar

iCalはご存知の通りAppleの作ったスケジュール管理ソフトでして、iCalendarというのは、そのデータ形式の規格のこと。くわしくは、RFC2445, 2446, 2447に記述されているわけです。英語はいや、という人は、こちらにおおよその仕様が日本語でまとめてあります。
一応ApplescriptでiCalのデータファイル(拡張子.ics)を直接扱えたりもするんですが、そのためにiCalを起動したりとか、そういう面倒なことはしたくなかったので、拙作のソフトCloCal ProおよびCloCalXは自前でiCalのデータを取り込んでおります。なんですが、いわゆる繰り返しルール「Recurrence Rule」について、というのが非常に厄介で、ま、ソースを無理なく構築するためにも、自分の頭を整理するためにも、とiCalに関係しそうな部分(あくまでもiCalに関係しそうな部分なんで、iCalendarの仕様をすべてカバーしてません)をまとめてみよう、と思ったわけです。このRecurrence Ruleについては、ざっとRFC2445の4.3.10と4.8.5.4に載ってます。詳しくは原文を。

はじめに

繰り返しルールっていうのは、つまり、「今日から3日間」とか「毎週月曜」とか「毎月1日と15日」とか「毎月第3日曜」とか、そういう感じのやつです。ようするに定期的なイベントを記述するルールのこと。どのパラメータもそうですが、こいつの場合は、

RRULE:Para1=Value1;Para2=Value2...

というのが基本形。Paraがそのルールの名前で、Valueが値、だと思ってればいいかと。このルールの組み合わせで、いろんな複雑なルールが出来上がってしまうから、大変なのです。
それで、まずは簡単なルールから書いていくと、FREQ、UNTIL、COUNT、INTERVALというのがあります。

FREQ

ようするに頻度。Valueとして、DAILY、WEEKLY、MONTHLY、YEARLYがあります。毎日とか毎週とか。

UNTIL

これは、期限です。いつまで、というもの。Valueとして日付データ(NSCalendarDateの形式でいえば例えば、%Y%m%dT%H%M%S)をとります。

COUNT

そのイベントが繰り返される回数です。UNTILとの組み合わせは存在せず、どちらかで期限を設定します。どちらも記述がない場合は、ようするに無期限、というわけです。Valueとして自然数をとります。1だと繰り返しなし、になります。

INTERVAL

間隔です。何日毎とか。Valueとして自然数をとって、FREQとの組み合わせで何日毎、何週間毎、なんかが設定できます。

つまるところ、おおよその簡単なルールだったら、上記の組み合わせで設定されているわけですね。UNTILとCOUNTは選択だから、3つのルールで記述されていると思ってもらえればよいかと。例えば、今日から3日間、とかいうのであれば、

RRULE:FREQ=DAILY;COUNT=3;INTERVAL=1

という感じ。

iCalでの操作と実際のデータ

iCalはiCalendarで定義されているすべてを使えるというわけじゃなくて、というか、使う必要もないわけなんですが、つまりiCal上でおこなった操作が実際にはどのようにデータに書き込まれているか、というのは見てみないとわからない。ので、まずは、見てみましょう。
2003年1月10日(金)に予定を作成して、「毎日」「2日ごと」「2003年2月10日終了」にすると、このときのデータ該当部分はこんな感じに、

RRULE:FREQ=DAILY;UNTIL=20030210T145959;INTERVAL=2

これは、今までの説明で解読可能ですね。で、ここから問題ですが、「毎週」以降を選択すると、もっといろいろ設定できるようになってしまいます。先程と同様に、「毎週」「金曜」「2週ごと」「2003年2月10日終了」にすると、

RRULE:FREQ=WEEKLY;UNTIL=20030210T145959;INTERVAL=2;BYDAY=FR

最後にBYDAYというのが追加されました。これが後々にくせ者になります。今のところは、これで曜日(FRは金曜の意)を指定してると思えば大丈夫。さらに、「毎月」なんて選択すると、さらにややこしく、曜日指定だけじゃなくて、日付指定もできるようになってしまいます。といっても、両方同時には指定できない。しかも曜日指定は、週も含む。って、これは「毎月」のイベントなんで、月に1回だけ、だから当たり前ですが。今度は「毎月」「1月ごと」「日付指定:10日」「回数2回」としまして、

RRULE:FREQ=MONTHLY;COUNT=2;INTERVAL=1;BYMONTHDAY=10

となります。BYMONTHDAYというのが、日付指定になりますね。このあたりのルールについて詳しくは後ほど個別にやっていきます。で、曜日指定の場合だと、例えば、「毎月」「1月ごと」「曜日指定:第2金曜」「回数2回」とすれば、

RRULE:FREQ=MONTHLY;COUNT=2;INTERVAL=1;BYDAY=2FR

BYDAYのFRの前についた数字が週を指定します。最後に「毎年」はというと、月指定と曜日指定ができます。こちらは同時に指定可能。まずは、「毎年」「1年ごと」「月指定:1月」「回数2回」として、

RRULE:FREQ=YEARLY;COUNT=2;INTERVAL=1;BYMONTH=1

BYMONTHが月指定。これだと毎年の1月10日にイベントがあることになるわけです。次に、「毎年」「1年ごと」「月指定:1月」「曜日指定:第2金曜」「回数2回」とすれば、

RRULE:FREQ=YEARLY;COUNT=2;INTERVAL=1;BYDAY=2FR;BYMONTH=1

こうなって、これは、毎年1月の第2金曜にイベント、というわけ。奥が深い。
さて、ここで問題にしたいのは、そもそも予定を設置した日とその後の繰り返しイベントの曜日指定なんかが違っている場合。えーと、つまり、たとえば、2003年1月10日(金)に予定を作成したのに、「毎年」「1年ごと」「月指定:1月と7月」「曜日指定:第1日曜」「回数4回」とかした場合、

RRULE:FREQ=YEARLY;COUNT=4;INTERVAL=1;BYDAY=1SU;BYMONTH=1,7

こういう感じになってしまいます。(BYMONTHのあとのコンマ区切りは複数選択の場合:これもあとで詳しく)これだけを見ると、単に毎年1月と7月の第1日曜にイベント、ってことなんですが、スタートするのは2003年の1月10日(金)なわけで。そこら辺がごっちゃになってしまうんですよね。この場合、実際にiCalのスケジュールを先のほうまで追いかけると、イベントがあるのは2003年1月10日(金)と2003年7月6日(日)、2004年1月4日(日)となって、2004年7月4日(日)を最後に繰り返し終了。最初だけ第2金曜であとは第1日曜、になってしまうんです。こいつは面倒・・・

というところで、とりあえず、新しくわいてきた各ルールの説明を

BYDAY

曜日指定、になります。2文字のアルファベットで曜日を指定します。日曜=SU、月曜=MO、火曜=TU、水曜=WE、木曜=TH、金曜=FR、土曜=SAという感じで。MONTHLYとYEARLYの場合は何週目かも指定されるのでアルファベットの前に数字がつきます。数字は-1と1から4まで。正数の方はそのまま第何週、に対応してますが、-1は最終週に対応。こいつも面倒。
これは、BYDAY以下すべてに当てはまりますが、値をコンマで区切ることで複数の指定を可能にしています。(とはいえ、BYDAYの複数選択はWEEKLYのイベントのみ)

BYDAY=SU,SA

これで、土日指定。BYDAYはDAILY以外で指定されますが、WEEKLYの時は必須、MONHTLYならBYMONTHDAYと二者択一で指定、YEARLYで指定した場合はBYMONTHと組み合わされます。

BYMONTHDAY

これは日付指定。月のうちの何日かを数字で指定します。MONTHLYのみで指定されて複数選択もあり。BYDAYと二者択一で指定。

BYMONTH

これは月指定。年のうちの何月かを数字で指定します。YEARLYのみで必ず指定されて複数選択もあり。

ほんとはこれ以外にもiCalendarとしては、BYHOURみたいな時間指定なんかがあったりしますが、iCalには関係ないので割愛。

以上3つをまとめるとこういう感じ?

ルール 毎日
(DAILY)
毎週
(WEEKLY)
毎月
(MONTHLY)
毎年
(YEARLY)
BYDAY
(曜日指定)
× ◎ (※1) △(※2) ○(※2)
BYMONTHDAY
(日指定)
× × △(※3) ×
BYMONTH
(月指定)
× × × ◎(※3)

◎:必須、○:指定可、△:二者択一で指定可、×:指定なし
※1:複数可/週指定なし
※2:複数不可/週指定あり
※3:複数可