カレンダーにまつわる標準まとめ

これは [TimeTree Advent Calendar 2023](https://qiita.com/advent-calendar/2023/timetree) の20日目の記事です。

https://qiita.com/advent-calendar/2023/timetree

こんにちは、 TimeTree の元バックエンドエンジニアの @lciel です。
初開催のアドベントカレンダーに参加したすぎて、とんでもなく久しぶりに記事を書きました。

今回はカレンダーや日時表現にまつわる標準についてまとめてみました。
カレンダーを作るときや、グローバル向けのサービスで日時を扱うときなどに参考になれば幸いです！

内容に間違いがあったら指摘していただけると嬉しいです :pray:


# :calendar: iCalendar(RFC 5545)

カレンダーを扱う上で重要な標準である、 Internet Calendaring and Scheduling Core Object Specification 通称 iCalendar です。
[RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545) で策定されています。

https://datatracker.ietf.org/doc/html/rfc5545

iCal と略されて紹介されることも見かけますが、過去に macOS に搭載されていた標準カレンダーアプリである iCal とは別物です。
(ちなみに Mac OS X Mountain Lion から、標準カレンダーは「カレンダー(Calendar)」になりました)

初版は [RFC 2445](https://datatracker.ietf.org/doc/html/rfc2445) で、業務グループウェアの Lotus(IBM) や Microsoft によって1998年に制定されています。
新版の RFC 5545 は2009年に制定されていて、 RFC 2445 を改定した内容になっていますが、[主な差分](https://datatracker.ietf.org/doc/html/rfc5545#appendix-A)は繰り返しのルールである RRULE 周辺の不都合を整えたマイナーバージョンアップといった印象のものです。

```text:iCalendarの例(macOS のカレンダーより)
# カレンダーの中身は VCALENDAR に内包される
BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:テストカレンダー
PRODID:-//Apple Inc.//macOS 12.3.1//EN
X-APPLE-CALENDAR-COLOR:#0E61B9
X-WR-TIMEZONE:Asia/Tokyo
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE

# タイムゾーンの定義
TZID:Asia/Tokyo
BEGIN:DAYLIGHT
TZOFFSETFROM:+0900
RRULE:FREQ=YEARLY;UNTIL=19510505T150000Z;BYMONTH=5;BYDAY=1SU
DTSTART:19500507T000000
TZNAME:JDT
TZOFFSETTO:+1000
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+1000
DTSTART:19510909T010000
TZNAME:JST
TZOFFSETTO:+0900
END:STANDARD
END:VTIMEZONE

# 予定の定義
BEGIN:VEVENT
CREATED:20231218T082704Z
UID:DD11C617-8A3B-4579-B8AA-51727E64AE40
DTEND;TZID=Asia/Tokyo:20231218T190000
TRANSP:OPAQUE
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
SUMMARY:テスト予定
LAST-MODIFIED:20231218T082709Z
DTSTAMP:20231218T082714Z
DTSTART;TZID=Asia/Tokyo:20231218T180000
SEQUENCE:1
END:VEVENT

END:VCALENDAR
```

スケジュールの表現のために必要な、日時表現・タイムゾーン・繰り返し表現(後述)などはもちろん、コラボレーションのためのユーザー情報(主催者や参加者など)、ToDoやジャーナル(特定の日付につけられたドキュメント)、空き時間の表現などさまざまな機能の実現が含まれており、全体を通すとアプリケーションの仕様書に近いような内容になっています。

```text:ToDoアイテムの例(RFC 5545より)
BEGIN:VTODO
UID:20070313T123432Z-456553@example.com
DTSTAMP:20070313T123432Z
DUE;VALUE=DATE:20070501
SUMMARY:Submit Quebec Income Tax Return for 2006
CLASS:CONFIDENTIAL
CATEGORIES:FAMILY,FINANCE
STATUS:NEEDS-ACTION
END:VTODO
```

その機能範囲の広さから iCalendar に対応するライブラリも RFC の内容すべてをサポートしていることは少なく、一部機能をサポートしている場合が多い印象です。(たとえば後述の RRULE でも秒単位での繰り返しはサポートしない、など)
また参加者のようにリソースを指定するものには mailto スキームでのみ指定可能など、当時の背景を感じる内容となっています。

歴史のある標準ではありますが、 iCalendar は現在でも多く使われていて、 2021年にも iCalendar の拡張が策定されているようです。

https://datatracker.ietf.org/doc/html/rfc9073

拡張子は `ics` で、カレンダーソフトウェアのエクスポート/インポートに iCalendar 形式のファイルが用いられるケースがとても多いです。
MIME Type も `text/calendar` が割り当てられて多く実装されているため、 HTTP 等を通じてカレンダーアプリに予定を登録するなど、 iCalendar を通じることで標準的にスケジュール情報をやり取りすることができるというのも大きいと思います。

どんなデータ構造でカレンダーを表現できるかわかるため、カレンダーを作る際には一読しておくとかなり参考になると思います。

## :repeat: 繰り返しルール: Recurrence Rule(RRULE)

カレンダー3大無くなって欲しい問題の1つに繰り返し予定があります。
[Recurrence Rule(RRULE)](https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10) は iCalendar のうち、繰り返し予定を表現するための規格です。

ここで言う繰り返し予定とは、カレンダーで設定できる「2日ごと」「毎週月曜日」「毎月第3金曜日」といったような、特定のパターンで同じ予定を繰り返して設定することができるものを指しています。

たとえば以下は「毎年1月5日のAM8:30」という繰り返しパターンを表現するものです。

```text:毎年1月5日のAM8:30
DTSTART;TZID=America/New_York:19970105T083000
RRULE:FREQ=YEARLY
```

頻度や条件を表すプロパティを使うことで、あらゆる繰り返しパターンを表現することができます。
たとえば以下では「1年おき(2年に1回)で、1月の毎週日曜、AM8:30およびAM9:30」という繰り返しルールになります。

```text:1年おき、1月の毎週日曜、AM8:30およびAM9:30
DTSTART;TZID=America/New_York:19970105T083000
RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30
```

また、特定の日付を指定して除外する `EXDATE` や、RRULE を指定することで繰り返しパターンで除外する `EXRULE` を組み合わせることで、繰り返されている予定のうち「この日を削除」することもできます。
ただし `EXRULE` は新版(RFC 5545)では Deprecated になっています。

iCalendar 全般に言えることですが、特に RRULE は複雑なこともあり、規格に沿っていないデータが入りやすい印象があります。
iCalendar 形式で入ってくるものが正しい規格であると鵜呑みにすると、あとでエラい目にあうかもしれないので気をつけてください。(土下座)

## :globe_with_meridians: CalDAV(RFC 4791)

Calendaring Extensions to WebDAV 通称 CalDAV は、WebDAV を経由して iCalendar をやりとりするための、カレンダーサーバーのための標準です。
[RFC 4791](https://datatracker.ietf.org/doc/html/rfc4791) で策定されています。

https://datatracker.ietf.org/doc/html/rfc4791

この標準に対応することで、 iCalendar を WebDAV を通じて publish したり subscribe することができるようになります。
たとえば iOS のカレンダーは CalDAV に対応していて、 `設定 > カレンダー > アカウントを追加 > その他 > CalDAV` から設定することでカレンダーアカウントとして利用することができます。

<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11679/ad09ee08-484f-c02a-863a-7395d6873bb3.png">

ただ最近は WebDAV 自体が Windows で非推奨とされる方針のようで、 CalDAV も非推奨の方向へ向かうのかもしれません。

https://forest.watch.impress.co.jp/docs/news/1544482.html

# :clock: 日時の表記(ISO-8601 / RFC 3339)

日時の表記はカレンダーに限らず、多くの場面で利用するので、この記事の中では最も一般的な標準かと思われます。

[ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html) や [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) で策定されていますが、  ISO-8601 は広範な規格であるのに対して、 RFC 3339 は ISO-8601 を元にした「インターネット上での日時の表記を表すため」の規格という違いがあります。
そのため RFC 3339 は ISO-8601 のサブセットというような形になります。

https://www.iso.org/iso-8601-date-and-time-format.html

https://datatracker.ietf.org/doc/html/rfc3339


ISO-8601 や RFC 3339 では、日時表記においてオフセットとして時差を表現することができます。
ちなみにオフセット 00:00 である UTC を表す Z は、 Zero ではなく Zulu Time(UTC の軍名)です。

```text:日時の表記例
# 2023-12-20T01:23:45Z(UTC)
%Y-%M-%DT%h:%m:%sZ

# 2023-12-20T01:23:45+09:00(+9時間の時差)
%Y-%M-%DT%h:%m:%s%Z:%z
```

サブセットという位置づけでありつつも、実は RFC 3339 でのみ利用可能なフォーマットも存在します。
ISO-8601 と RFC 3339 の違いは以下のサイトがとてもわかりやすいです。
(ただし、特殊な事情がない限りは重複しているフォーマットを使うのが無難と思われます)

https://ijmacd.github.io/rfc3339-iso8601/

# :earth_americas: タイムゾーン: IANA Time Zone Database(tz database)

カレンダー3大無くなって欲しい問題の1つである、タイムゾーンを扱うための標準です。
タイムゾーンについては @gonsee の記事でも紹介されています。

https://qiita.com/gonsee/items/f26798f841efc338d899


ISO-8601 でもタイムゾーンからくる時差はオフセットで表現されますが、それがどのタイムゾーンであるか(たとえば +09:00 は日本標準時であるか韓国標準時であるか、パラオ標準時であるかの区別はつきません)や、タイムゾーンに付随したサマータイム(DST)の表現は入っていません。
その時刻を表現するのであれば ISO-8601 で十分なケースも多いですが、カレンダーのように継続した日時を扱うためにはタイムゾーンという情報を扱うことが必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11679/4e6b1b9c-b468-d050-bd60-5d329be2b71d.png)
(世界が象の上で平らにあるのであれば、この苦しみは生まれなかったのに)

`Asia/Tokyo` のような表現を見かけたことがある人は多いと思いますが、これは Time Zone Database で表記されたタイムゾーンです。
Time Zone Database は、他にも tz database, tzdata, zoneinfo database などさまざまな表記で表示されることがありますが、すべて同じものです。

もともとボランティアで始まったプロジェクトですが、現在は [IANA が管理](https://www.iana.org/time-zones)していて、現在の最新版は 2023/03/28 にリリースされた `2023c` です。

情報処理におけるタイムゾーンについては、「これが標準！」と定められているものがなく、最も利用者・採用箇所が多いデファクトスタンダードとして Time Zone Database があるという理解をしています。
Time Zone Database も RFC で標準化はされていません。
(ただし Time Zone Database をメンテナンスするための [RFC 6557](https://datatracker.ietf.org/doc/rfc6557/) はあります。なぜ。)

たとえば先の iCalendar でもこの Time Zone Database が表記として使用されることが多いです。
しかし iCalendar はオフセットなどのタイムゾーンの定義を内部で記述して内包する構造になっていて、 Time Zone Database を参考にすると良いというレベルで触れられているのみで、規格に含まれているわけではありません。
そのため Time Zone Database には含まれない `Asia/Osaka` のようなタイムゾーンを iCalendar 内で見かけることがあります。

Time Zone Database は glibc を始めとした、様々なライブラリに内包されています。
macOS や Linux では `/usr/share/zoneinfo` に zoneinfo としてインストールされており、多くのライブラリがタイムゾーンとしてこの zoneinfo を参照しています。
そのため、タイムゾーンを扱う際には Time Zone Database を標準として選択するのが最も良いのではないかと思います。

## :beach: サマータイム(夏時間, DST: Daylight Saving Time)

タイムゾーンには「日光を有効利用しよう」という目的で、サマータイムが設定されている場合があります。
サマータイムは夏を中心とした季節において、時刻を1時間(または30分)早める制度です。

Time Zone Database にはサマータイムの情報も含まれているため、タイムゾーンを指定することでサマータイムも扱うことができます。
```ruby:サマータイム切り替えの様子
# 開始時は時間が飛ぶ(1:59 => 3:00)
> Time.parse('2023/03/12 09:59 UTC').in_time_zone("America/Los_Angeles")
=> Sun, 12 Mar 2023 01:59:00.000000000 PST -08:00
> Time.parse('2023/03/12 10:00 UTC').in_time_zone("America/Los_Angeles")
=> Sun, 12 Mar 2023 03:00:00.000000000 PDT -07:00

# 終了時は時間が巻き戻る(1:59 => 1:00)
> Time.parse('2023/11/05 08:59 UTC').in_time_zone("America/Los_Angeles")
=> Sun, 05 Nov 2023 01:59:00.000000000 PDT -07:00
> Time.parse('2023/11/05 09:00 UTC').in_time_zone("America/Los_Angeles")
=> Sun, 05 Nov 2023 01:00:00.000000000 PST -08:00
```


Time Zone Database には過去の時刻制度の変更経緯などもすべて含まれています。
たとえば日本も戦後の1948年から4年間だけサマータイムが導入されたことがあったのですが、この日時を扱うときにはサマータイムを考慮する必要があります。
Time Zone Database を使うと、日本におけるかつてのサマータイムの情報も扱うことができます。

```ruby:過去に巻き戻ると、日本のサマータイムの適用が確認できる
> Time.parse('1947/08/10 00:00 UTC').in_time_zone('Asia/Tokyo')
=> Sun, 10 Aug 1947 09:00:00.000000000 JST +09:00

# 期間中は JST ではなく見慣れない JDT(Japan Daylight Saving Time) が適用されている
> Time.parse('1948/08/10 00:00 UTC').in_time_zone('Asia/Tokyo')
=> Tue, 10 Aug 1948 10:00:00.000000000 JDT +10:00

> Time.parse('1952/08/10 00:00 UTC').in_time_zone('Asia/Tokyo')
=> Sun, 10 Aug 1952 09:00:00.000000000 JST +09:00
```

## :point_up:  余談

Time Zone Database では `<地域>/<地名>` の形で命名され、地名には国ではなく Tokyo のように都市や諸島の名前が用いられます。
その理由として、国名は政治的な原因で変わることがあり、都市名は変わりにくいためとされています。

一方で昨年のバージョン `2022b` において、 `Europe/Kiev` を `Europe/Kyiv` に変更するという、政治的原因による変更が行われました。

https://github.com/eggert/tz/commit/e13e9c531fc48a04fb8d064acccc9f8ae68d5544

`Europe/Kiev` から `Europe/Kyiv` へのリンクが残されているため同一システム内では問題は起きづらいですが、先にアップデートされたシステムから記録されたタイムゾーンを下位バージョンで扱えないという問題は発生します。
実際に TimeTree でもこの影響による問題が発生して、対応が行われました。


# :end:  おわりに

TimeTree では、カレンダー3大無くなって欲しい問題に一緒に取り組んでくれる仲間を募集しています！
興味をもっていただけた方は、残り1つが一体何なのかカジュアル面談で聞いてみてください。

https://timetreeapp.com/intl/ja/corporate/careers