icalendarでGoogle Calendarのイベント情報を抽出する

Google Calendarに登録したイベント情報を、再利用のために抽出することを考えます。
Google Calendar Data APIを使う方法もあるようですが、ここではiCalendar形式でエクスポートしたカレンダー情報を、Rubyのicalendarライブラリを用いて抽出する方法を取ります。

作業環境は以下の通り。

icalendarライブラリのインストール

準備作業として、iCalendar形式のカレンダー情報を読み書きするRubyライブラリicalendarをインストールします。

# gem install icalendar

iCalendar形式でのエクスポート

Google Calendarからデータをエクスポートするには、カレンダー設定ページ (「マイ カレンダー」下のカレンダー名をマウスオーバーすると出てくる三角形をクリック) の「カレンダーのアドレス」にあるボタンをクリックします。

フォーマットとして、XMLICAL (iCalendar)、HTMLの3つの選択肢があり、ここではICALを選択します。

ボタンを押すとURLが表示されるので、open-uriなどを使って直接読み込むこともできます。ここでは、指定したURLからiCalendarデータを一旦ローカルにダウンロードし、ファイル (拡張子ics) として読み込む方法を取ります。

iCalendarデータの読み込み

icalendarライブラリによるicsファイルのパーズ自体は、こんな感じで簡単にできます。

require 'icalendar'

ical_src = IO.read("basic.ics")
cal = Icalendar.parse(ical_src, true)

Icalendar.parseは、引数で指定したiCalendar形式の文字列をパーズしてIcalendar::Calendarオブジェクトに変換します。
ここでのちょっとした落とし穴として、Icalendar.parseの第2引数を省略すると、戻り値はCalendarオブジェクトを要素とする配列になります。
(iCalendarフォーマットをちゃんと調べていませんが、複数のカレンダーから構成されるiCalendarデータを扱えるらしい)

詳細はicalendarライブラリの公式ページにて。ただしIcalendar.parseの第2引数については記載がない模様…
http://icalendar.rubyforge.org/

カレンダーに含まれるイベント情報は Calendar#event で参照できます。

cal.events.each do |event|
  puts "--------------------------"
  puts "項目名: #{event.summary}"
  puts "開始時刻: #{event.start}"
  puts "終了時刻: #{event.end}"
  puts "場所: #{event.location}"
  puts "備考: #{event.description}"
end

実行例:

--------------------------
項目名: 作戦会議
開始時刻: 2011-12-10T08:30:00+00:00
終了時刻: 2011-12-10T12:00:00+00:00
場所: ◯×公民館 大会議室
備考: 
--------------------------
項目名: 定例会
開始時刻: 2011-12-03T08:30:00+00:00
終了時刻: 2011-12-03T12:00:00+00:00
場所: ◯×公民館 和室
備考: 

タイムゾーンの扱い

上の結果を見ると、開始時刻・終了時刻の時差が「+00:00」となっていて、期待したものとちょっと違います。

状況を見るために、まずはGoogle Calendarが出力したicsファイルを覗いてみました。先頭部分は以下のようになっています。

BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:test calendar
X-WR-TIMEZONE:Asia/Tokyo
X-WR-CALDESC:
BEGIN:VEVENT
...

一応タイムゾーン情報として、X-WR-TIMEZONE:Asia/Tokyo という記述が含まれているようです。

さらにイベント情報を見てみると、こんな感じになっていました。

BEGIN:VEVENT
DTSTART:20111210T083000Z
DTEND:20111210T120000Z
DTSTAMP:20111218T010349Z
UID:(省略)@google.com
CREATED:20111203T054949Z
DESCRIPTION:
LAST-MODIFIED:20111203T055007Z
LOCATION:大会議室
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:作戦会議
TRANSP:OPAQUE
END:VEVENT

上のDTSTARTとDTENDが、Rubyコード上のEvent#startとEvent#endにそれぞれ対応し、ここにはタイムゾーン情報は入っていないようです。
(iCalendarを規定しているRFC5545ではDTSTART/DTENDにタイムゾーン情報を記述できるようなので、これはGoogle Calendar固有の仕様か)

icalendarライブラリの挙動としては、X-WR-TIMEZONEは単純に無視されて、元のカレンダーにはあったタイムゾーン情報が反映されないという結果になるようです。

ここは、安直に、Event#startとEvent#endで取得したDateTimeオブジェクトに決め打ちで時差情報を追加するという手を取りました。

  puts "開始時刻: #{event.start.new_offset(9.0/24)}"
  puts "終了時刻: #{event.end.new_offset(9.0/24)}

実行例:

--------------------------
項目名: 作戦会議
開始時刻: 2011-12-10T17:30:00+09:00
終了時刻: 2011-12-10T21:00:00+09:00
場所: ◯×公民館 大会議室
備考: 
--------------------------
項目名: 定例会
開始時刻: 2011-12-03T17:30:00+09:00
終了時刻: 2011-12-03T21:00:00+09:00
場所: ◯×公民館 和室
備考: 

イベントのソートとフィルタリング

ここまでで一応の目的は達成できましたが、後処理として、以下の加工を行うことを考えます。

  • イベントを日付の早い順に並び替える
  • 今後のイベントのみ抽出する

Google CalendarからエクスポートしたiCalendarデータは、イベントの日付の遅い順 (あるいは最近登録された順?) に並んでいるようです。これを、Array#sortを使って日付の早い順に並びかえます。DateTimeは <=> メソッドが使えるので、これはカンタン。

cal = Icalendar.parse(ical_src, true)
events = cal.events.sort {|e1, e2| e1.start <=> e2.start}

開始時刻が現在時刻より後のイベントのみ抽出するには、Array#select (正確にはEnumerable) と、DateTime.nowの組み合わせが使えます。

cal = Icalendar.parse(ical_src, true)
events = cal.event.select {|e| e.start > DateTime.now}

イベントのソートとフィルタリングを組み合わせて、以下のようになります。一旦DateTimeに変換してしまえば後は楽ちんです。

cal = Icalendar.parse(ical_src, true)
events = cal.events.select {|e| e.start > DateTime.now}
  .sort {|e1, e2| e1.start <=> e2.start}