RiCalはiCalendar形式としてよりよく知られているRFC2445の実装である。iCalendar形式を読み書きするRubyライブラリはいくつかすでに存在しているが、Rick DeNatale氏は新たなライブラリを書くことを決めた。私たちは彼と話し、なぜ彼がそのプロジェクトを始め、彼がどんな障害にぶつからざるを得なかったかを聞いた。
カレンダー形式の実装はそれほど楽しそうなものではないように思いますが、なぜあなたはRicalを始めようと思ったのですか?
私の最も最近のフルタイムの仕事の時にこの問題に興味を持ったのです。私は昨年のほとんどの期間あるスタートアップ企業で働いていましたが、その企業はチーム/グループ協調機能をソフトウェアサービスとして提供する大きなRailsアプリケーションを持っていました。そのアプリケーションは、既存のカレンダーコンポーネントを使っていたのですが、いくつかの理由でそれは不十分なように思えました。UIはGoogleカレンダーのような競合に匹敵するものではありませんでしたし、外部のカレンダーアプリケーションからiCalendarファイルをインポートする要求もありました。
私はUIに対しては数多くの作業を行いました。その作業は数多くのJavascriptとブラウザーの移植性のデバッグを含む興味深いものでした。
しかし、一般的なiCalendarファイルを適切にインポートできるようにするためにかかるコストは、予算を超えてしまうことがわかりました。その時点で、iCalendar形式(それはRFC2445で定義されています)に対するサポートの目安となる2つの人気のあるRuby gemがあり、それらは今でもありますが、両方ともiCalendarデータを解析し、はき出すことは十分にこなしてくれるのですが、そのセマンティクスの実装においてはたいしたことは出来ませんでした。
最も大きな欠陥の一つは繰り返しイベントのサポートがなかったこと、それに関連したこととして、RFCに定義されたタイムゾーンのサポートがなかったことです。
その会社は資金を使い果たして開発事業を止めてしまったので、私は行き当たりばったりにお金のもらえる仕事を探し続けていましたが、その一方で、より多くの時間が出来たのでRiCalの作業をやりたくなっていました。
RFCはかなり大きく複雑ですが、あなたはどこから始めたのですか?
はい、それは確かに「委員会によって設計されたラクダ」です。その推進者はMicrosoftとLotus Notes/IBMでした。私はIBM時代に、Smalltalk、Javaなどに関する別の同じような標準化団体に関わっていましたので、そんな獣がどのように育てられたのかわかりますし、その動物をどう扱うかもある程度はわかります。
私が最初に見たのは繰り返しイベントの問題でした。それは、そこに興味があったからです。RFC 2445は繰り返しをEvent、Journal entry、Todoのいずれかの事象から時刻または期間を生成したり取り除いたりする、複雑な規則やリストの組み合わせを利用して定義しています。さらに、iCalendarのカレンダーの中ではタイムゾーンは同じメカニズムを利用して標準時間と夏時間の移行を定義しています。
スクラッチから始める必要がありましたか、それとも、断片的にでも再利用できるものがあったのでしょうか?
繰り返しを扱えるRuby gemは2つありました。Matthew Lipper氏のRuntとJim WeirichのTexpです。ともにMartin Fowler氏の時称(Temporal Expression)パターンをベースにしています。Martin氏が行ったことは委員会よりもずっとすっきりとしています。時称はおそらく80/20則を示しています。20%の作業・複雑さで80%のことを成し遂げているのです。
私は、その2つのライブラリの一方を使って事象の列挙を実装しようとした数人の友人や同僚が、それほど楽しい時間を過ごせなかっただけでなく、望んだ目的も達成できなかったことも知っています。問題はMartin氏のパターンによるものではなく、「ラクダ」によるものなのです。
その繰り返しイベントというのはどのようなものなのですか?
RFC 2445のもっとも複雑な部分が繰り返し規則で、たとえば毎日とか2ヶ月に1回といった、基本的な頻度や間隔の仕様を定めています。この規則がある開始時刻に適用されると、それによって一連に時刻の修正が発生します。それだけであれば十分に簡単なものですが、規則はその月の2番目と最後の月曜日というような頻度設定も加えられるようになっています。これはBYWDAY=2MO,-1MOのように表現されるもので私はコードの中でbypartと呼んでいます。bypartはいろいろな方法でbypart同士、または、基本の頻度や間隔と組み合わされたり、相互作用したりします。bypartの頻度の関係に応じて、bypartは付加的な事象を追加したり、それを取り除いたりすることもできます。例として、アメリカ合衆国の大統領選挙がいつ行われるかを1996年の選挙を開始時点として表現したイベントは次のようになるでしょう:
DTSTART;TZID=US-Eastern:19961105T090000RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 法律では、アメリカ合衆国の大統領選挙は4で割り切れる年の11月の最初の月曜の後の最初の火曜日に行われることになっています。
実のところたった今私が出した例はRFC2445からそのままもってきたものなのです。
これは本当にかなり複雑なように見えますが、この規則をどうやって解析していますか?
私はこれらの規則をどのように解釈するかを考えるのに非常に長い時間をかけて、最初の動くアルゴリズムを思いつきました。その基本的なアプローチは、最後の事象の時刻が与えられたらその次の事象の時刻が生成されるというメソッドを思いつくことでした。私の最初の取り組み方では、次の事象がその秒を変更する必要があるか、その分を変更する必要があるか、と基本の頻度のレベルまで到達するまで続け、その後で基本の頻度よりも長い期間を持ったbypartにマッチしない時刻を取り除くような一連のメソッドを持った単一の列挙クラスを利用していました。
私は仕様にある繰り返し規則の例のそれぞれに対してRSpecの例を書き、それらがすべて動くことを確認しました。そして、それらはかなり効率的に動いていました。完全な仕様のテストスイートが私のMacBookで13秒かそこらで動いたのです。
それを完成させた後、次に何をしましたか?
次のステップはタイムゾーンに取り組むことでした。一般にタイムゾーンはやっかいなものです。名前づけに関する真の標準はなく、それらが何なのか、ということに関してもたくさんの混乱があります。タイムゾーンを扱ってきた多くのRuby開発者は、たいていTZInfo gemになじみがあると思います。TZInfo gemは多くの現代的なオペレーティングシステムで共通に使われているOlsonタイムゾーンデータベースへのアクセスを提供します。よいかわるいかは別として、Olsonデータベースはタイムゾーンに対するたくさんの別の識別子をもっているので、ユニークではありません。また、RFC2445は標準化されたタイムゾーン識別子に関する問題を棚上げしていて、その結果としてタイムゾーンをiCalendarを利用するアプリケーション間で参照としてやりとりすることができず、iCalendarデータの中にそれらの定義を含めなければならないのです。
そこで、RiCalはVTIMEZONEコンポーネントを表現するクラスを含んでいて、それによって実際にUTCとそのインスタンスによって表現されたタイムゾーンとの間での時刻の変換が可能になっています。また先ほど述べたように、ある時刻が標準時間のものなのか夏時間のものなのかを決定するためには正しい期間を見つける繰り返し規則を使うことが要求されます。
そのことを別の見方をすると、たとえばRiCalを使ってあなたのRuby/Railsアプリケーションでイベントをつくったら、他のアプリケーションでも理解できるiCalendar形式のデータとしてそのイベント情報を出力することができるということです。そのイベント情報は、そのイベントが必要とするタイムゾーンを定義しているVTIMEZONEコンポーネントを含むカレンダーには、出力できなければいけません。そこでRiCalはTZInfoからそのタイムゾーンデータを取得するようなVTIMEZONEを表現するクラスも持っていて、VTIMEZONEコンポーネントの有効なRFC2445の外部表現を出力する方法も知っています。そのコンポーネントがどんな期間を持っていても、いつ標準時間と夏時間が移行するかを持っていても、です。
それら2つのハードルを乗り越えたあと、さらに何か問題がありましたか?
すべての作業を終えたとき、私は事象の列挙による方法が平均的には効率的に動くけれども、わずかのケースがその平均を悪い方向に押し下げているということに気づきました。そして残念ながらそれらのケースはタイムゾーンを定義している時に使われる傾向があることがわかったのです。多くのこのようなケースでは、1年のすべての日で可能性のある事象時刻を列挙しておいて、たとえば4月の最初の日曜といった特定の日に当たるもの以外は全部捨ててしまっていました。
そこで私は最初からやり直すことにしました。たぶんシャワーを浴びている時に、私はそこで私にとって最高の「そうか!」と思うような考えの多くを生み出すのですが、オブジェクトのチェーンを利用する新しいアプローチを思いつきました。それぞれのbypartと規則の中の頻度と間隔に対して一つずつのチェーンを、です。この方法はある特定の繰り返し規則が出てきた最初の時に効率的にハイブリッドなオブジェクトに「コンパイルできる」のです。そしてさらに重要なことはその方法によって4月の最初の日曜のようなケースのすべてで「早送り」ができるようになるということです。
リファクタリングをしてきっちり動くような状態に戻すにはかなりの時間がかかりましたが、結果としてその価値があったように思います。前に述べたテストスイートの実行に13秒くらいかかっていたのが約4?5秒で終わるようになったのです。そのスイートは少し大きくなっていましたが、それは基本的には繰り返しとは無関係の新しい関数の仕様を付け加えたからです。
あなたはカレンダーを作成するDSLもつくっていますが、それについて詳しく教えてもらえるでしょうか?
最近数週間、私はRiCalに対する「Builder DSL」の作業をしてきました。できるだけIcalendarとVpimという2つのgemと互換性があるようにつくろうとしてきたのですが、私は拡張して、誰かが十分新しいバージョンのRails(というよりは実際にはActiveSupport)を使っていたら、それらが持っているTimeWithZoneオブジェクトからタイムゾーンを取り出せるようにしました。さらに私はRiCalをRailsの中で使うかどうかに無関係になるようにも作業を行ってきました。私はActiveSupportを必要としていませんしーもしかしたら、私の仕事を少しくらい楽にしてくれるかもしれませんがー、それにTZInfo::Timezoneが必要になったとしても、それがTZInfo gemからのものなのかActiveSupportからのものなのかは気にしません。
今の段階ではDSLについていくつか完全には納得がいかないことがありますが、ここまで述べてきた問題が私がRubyForgeとGitHubに「公式に」RiCalを発表する前に残っていた最後の問題でした。
GitHub上のReadmeにはDSLの例も含まれています:
RiCal.Calendar do
event do
description "MA-6 First US Manned Spaceflight"
dtstart
DateTime.parse("2/20/1962 14:47:39")
dtend DateTime.parse("2/20/1962 19:43:02")
location "Cape Canaveral"
add_attendee "john.glenn@nasa.gov"
alarm do
description "Segment 51"
end
end
end
それではRiCalに関して残された作業はなんでしょうか?
先ほど述べたように、今ちょうどDSL APIに最後の手を加えようとしています。
その後はユーザの意見を聞こうと思っています。RiCalは、他のカレンダーアプリケーションとデータを交換するRubyアプリケーションを書くことをもっと簡単にするようにすべきですが、仕様の複雑だからといって必ずしもすべてのアプリケーションがすべての詳細さで同じように仕様を解釈する必要はないはずです。私は誰かが対応する必要のある何かを見つけてくれると期待しています。
次の作業は、ActiveRecordでカレンダーやカレンダーコンポーネントをモデリングするのを助けるようなrailsプラグインのようなものに関わる作業になるかもしれません。
そして、カレンダーユーザインターフェースを構築することに関してもまだいくらか作業の余地がありますが、その作業とActiveRecordプラグインは別々のようで実は関連したプロジェクトになりそうです。
Rick、話す時間を割いてくれて大変ありがとうございます。
RiCalに関するさらなる詳細についてはRick's Blogでみつけることができる。またGitHubでプレリリース版が利用可能である。