2016-05-29 ,

org-mode文書中の予定(タイムスタンプ)をGoogle Calendarへ登録する

Google Calendarとやりとり出来るようになったので、org-modeとGoogle Calendarとの間で必要となる道具立てを作成しています。

色々作成中ですがとりあえず。

misohena/gcal: Google Calendar Utilities for Emacs

gcal-org.el というのが中心になるのですが、その中のgcal-oeventオブジェクトがGoogle Calendarへ登録される予定を表します。

gcal-oeventオブジェクト

gcal-oevent(以下単にoevent)は、org文書を解析して得られた1つの予定(event)を表現するオブジェクトです。

オブジェクトの作成

オブジェクトは、例えば以下のようなコードで作成できます。

(setq oe
  (make-gcal-oevent
    :id "a84717b2-7c0b-4549-80f4-9477d14f975f"
    :ord 0
    :summary "田中さんと打ち合わせ"
    :ts-prefix "DEADLINE"
    :ts-start '(2016 2 3 nil nil)
    :ts-end '(2016 2 4 nil nil)
    :location "東京駅"))

また、org文書を解析して(そのリストが)得られたり、

(gcal-org-parse-file "~/my-schedule.org") ;;指定ファイルから
(gcal-org-parse-buffer) ;;現在のバッファから

オブジェクトをファイルへ保存して、それを読み込んで得られたり、

;; my-schedule.orgを解析したものをmy-schedule.gcal-cacheへ保存
(gcal-oevents-save "~/my-schedule.gcal-cache" (gcal-org-parse-file "~/my-schedule.org"))

;; my-schedule.gcal-cacheからoeventのリストを読み込み
(gcal-oevents-load "~/my-schedule.gcal-cache")

Google Calendar上のカレンダーから読み込んで得られたりします。

;; カレンダー(ID:example@gmail.com)から予定を読み込んでoeventのリストとして返す
(gcal-org-pull-oevents "example@gmail.com")

プロパティの取得

プロパティの取得は (gcal-oevent-プロパティ名) で行えます。

(gcal-oevent-id oe) ;;=> "a84717b2-7c0b-4549-80f4-9477d14f975f"
(gcal-oevent-summary oe) ;;=> "田中さんと打ち合わせ"

1オブジェクト=1タイムスタンプ

このオブジェクトはorg文書中の一つの(アクティブ)タイムスタンプに対応します。一つのヘッドラインに対応するのでは ありません 。 org-agendaが行うように、SCHEDULED、DEADLINE、その他記事中のアクティブタイムスタンプ一つにつき一つの予定をGoogle Calendarへ登録したかったからです。

そのため :id の他に :ord というプロパティを持っています。このプロパティは一つのエントリー中に現れたアクティブタイムスタンプの順番を表します。

IDの変換

Google Calendarの予定のIDはbase32hexで使う範囲の文字(0-9a-v)しか受け付けません(ハイフンが入っているとダメです)。 また、デフォルトではUUIDをbase32hexでエンコードしたものをIDとしています。

従って、oeventをGoogle Calendarへ登録するときは:idをbase32hexでエンコードしたものへ変換することにしました。

(downcase (gcal-uuid-to-base32hex "a84717b2-7c0b-4549-80f4-9477d14f975f"))
;; => "l13hfcjs1d2kj07kihrt2jsnbs"

(gcal-uuid-from-base32hex "l13hfcjs1d2kj07kihrt2jsnbs")
;; => "a84717b2-7c0b-4549-80f4-9477d14f975f"

しかしこれだけだと1エントリー中に複数のタイムスタンプがある場合にIDが重複してしまいます。

それを避けるために :ord が1以上の時は、その数をIDの後ろに付加することにしました。幸い文字数の制限は緩いので。

(gcal-oevent-gevent-id
  (make-gcal-oevent
    :id "a84717b2-7c0b-4549-80f4-9477d14f975f"
    :ord 1
    ... )) ;;=> "l13hfcjs1d2kj07kihrt2jsnbs00001"

(gcal-oevent-id-ord-from-gevent-id "l13hfcjs1d2kj07kihrt2jsnbs00001")
;; => ("a84717b2-7c0b-4549-80f4-9477d14f975f" . 1)

:ord が0のときは付加しません。Google Calendar上で作成した予定を取り込むときはその方が都合が良いからです。取り込んだイベントをorg文書中にインポートした後、元のGoogle Calendar上のイベントのIDを番号付きに修正する必要が出てしまいますので。

タイムスタンプの扱い

:ts-start や :ts-end は ( ) というリストで表現します。このリストは gcal-ts- で始まる関数で色々な処理が出来るようになっています。

はnilの場合があります。時間の入っていないタイムスタンプに対応します。

(gcal-ts-date-only '(2016 5 29 nil nil)) ;;=> t <2016-05-29 Sun>
(gcal-ts-date-only '(2016 5 29 12 34)) ;;=> nil <2016-05-29 Sun 12:34>

ややこしいのはts-endの扱いで、Google Calendar側では終了時刻はその時刻自身を含まない(exclusive)とされていますが、org-mode側では曖昧であることです。 org-modeは日付のみの場合はinclusive、時刻を含む場合はexclusiveなのだと思うので、それを前提に変換しています。

(gcal-ts-end-exclusive '(2016 5 1 nil nil) '(2016 5 1 nil nil)) ;; => '(2016 5 2 nil nil) 1day <2016-05-01 Sun>
(gcal-ts-end-exclusive '(2016 5 1 nil nil) '(2016 5 2 nil nil)) ;; => '(2016 5 3 nil nil) 2days <2016-05-01 Sun>--<2016-05-02 Mon>
(gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 1 10 00)) ;; => '(2016 5 1 10 0) 0hour <2016-05-01 Sun 10:00>
(gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 1 12 00)) ;; => '(2016 5 1 12 0) 2hours <2016-05-01 Sun 10:00-12:00>
(gcal-ts-end-exclusive '(2016 5 1 10 00) '(2016 5 2 10 00)) ;; => '(2016 5 2 10 0) 24hours <2016-05-01 Sun 10:00>--<2016-05-02 Mon 10:00>
;;逆は gcal-ts-end-inclusive

oeventとGoogle Calendarとのやりとり

oeventをGoogle Calendar APIへ送る(gcal-events-insertへ渡す)ための形式へ変換するには gcal-oevent-to-gevent を、その逆は gcal-oevent-from-gevent を使用します。

;; Send a oevent
(gcal-events-insert "example@gmail.com" (gcal-oevent-to-gevent oevent))

;; Receive a event as oevent
(gcal-oevent-from-gevent (gcal-events-get "example@gmail.com" "l13hfcjs1d2kj07kihrt2jsnbs"))

oeventのリストは gcal-org-push-oevents と gcal-org-pull-oevents で送受信できます。

;; Send all active timestamps in my-schedule.org
(gcal-org-push-oevents "example@gmail.com" (gcal-org-parse-file "~/my-schedule.org") nil)

;; Receive
(gcal-org-pull-oevents "example@gmail.com")

gcal-org-push-oeventsは差分抽出機能を備えており、旧リストから新リストへの差分(追加、削除、更新)のみをGoogle Calendarへ送ることが出来ます。

;; my-schedule.orgだけにあるものを追加し、.oldだけにあるものを削除し、内容が変わったものをパッチする。
(setq result-events
  (gcal-org-push-oevents
    "example@gmail.com"
    (gcal-org-parse-file "~/my-schedule.org") ;;new-events
    (gcal-org-parse-file "~/my-schedule.org.old");;old-events
))
;; result-eventsはサーバ上に残った予定のリスト。
;; エラーが無ければnew-eventsと同じ。
;; 追加に失敗すればその予定を載らないし、更新に失敗すればその予定は旧状態、削除に失敗すればその予定は残る。

;; my-schedule.orgにある予定を全て削除する。
;; new-eventsがnil(空リスト)なので、old-events内の予定に対応するものは一つもないので。
(gcal-org-push-oevents
  "example@gmail.com"
  nil
  (gcal-org-parse-file "~/my-schedule.org"))

oeventとOrg文書とのやりとり

org文書からoeventのリストを得る(解析する)には次のようにします。

(gcal-org-parse-file "~/my-schedule.org")
(gcal-org-parse-buffer)

逆にoeventを文字列化してorg文書へ挿入するには次のようにします。 (※ただし、:ordのことを考慮していないので注意が必要です)

;; 挿入先のorg文書を開いた状態で実行すること
(gcal-org-insert-string-after-headline
  (gcal-oevent-format oevent)
  "Inbox")

gcal-oevent-format は何も引数を指定しなければ gcal-org-oevent-template 変数に書いてあるテンプレートを使ってoeventを文字列化します。

gcal-org-oevent-template
"** %{summary}\nSCHEDULED: %{timestamp}\n:PROPERTIES:\n :ID: %{id}\n :LOCATION: %{location}\n:END:\n"
  "org-mode text representation of oevent."

Org文書とGoogle Calendarとのやりとり

oeventオブジェクトを介さずに直接やりとりする関数も作っています。

;; キャッシュ~/my-schedule.gcal-cacheから~/my-schedule.orgへの差分を
;; カレンダーexample@gmail.comへ適用します。
;; 成功した更新はキャッシュへ反映されます。
(gcal-org-push-file "example@gmail.com" "~/my-schedule.org" "~/my-schedule.gcal-cache")

;; キャッシュmy-schedule.gcal-cacheからカレンダーexample@gmail.comへの差分を
;; ~/my-schedule.orgへ適用します。
;; 新しく追加された予定はヘッドライン"Inbox"の下に挿入します。
;; 成功した更新はキャッシュへ反映されます。
(gcal-org-pull-to-file "example@gmail.com" "~/my-schedule.org" "Inbox" "~/my-schedule.gcal-cache")

pullは作り途中で、更新に対応していません(2016-05-29現在)。org文書中の一部の場所だけ書き換えないといけないので。

oeventsリストの差分抽出

gcal-oevents-diffを使用すると二つのoeventリストを比較できます。

それぞれの予定を :id と :ord をキーにマッチングを行い、変化(追加、削除、変更、そのまま)を見つけ出し、それに対応する関数を呼び出します。

(gcal-oevents-diff
 (gcal-oevents-load "~/my-schedule.org") ;;old-events
 (gcal-org-pull-oevents "example@gmail.com") ;;new-events
 (lambda (old-oe new-oe) (insert (format "mod %s => %s\n" old-oe  new-oe)))
 (lambda (new-oe) (insert (format "add %s\n" (gcal-oevent-summary new-oe))))
 (lambda (old-oe) (insert (format "del %s\n" (gcal-oevent-summary old-oe))))
 (lambda (oe) (insert (format "not change %s\n" (gcal-oevent-summary oe)))))