タグ「JSR 310」が付けられているもの

Puzzle : JapaneseDate

この記事は Java Puzzlers Advent Calendar 2016 の 24 日目です。Date and Time API のローカル暦に関する出題です。

問題

以下の文を実行した結果は次のどれでしょうか?

System.out.println(LocalDate.from(JapaneseDate.of(JapaneseEra.MEIJI, 5, 12, 31)));

(1) 1872-12-02

(2) 1872-12-31

(3) 1873-01-01

(4) DateTimeException がスローされる

解答

(4) DateTimeException がスローされる

解説

これは JapaneseDate クラスの仕様です。もしくは、和暦に関する常識の問題。

日本では明治6年からグレゴリオ暦を導入しています。明治以降の紀元のみサポートされ、明治6年1月1日より前の日付はサポートされません。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/chrono/JapaneseDate.html より

Date and Time API のローカル暦はフィールドに設定できる値が標準 (ISO 8601) とは異なる場合があります。例えば HijrahDate では dayOfMonth に設定できる値は 1~30 に限定され、JapaneseDate では JapaneseEra (元号) によって設定可能な year、month、dayOfMonth の範囲が決まっています (例えば、昭和 64 年 1 月 8 日は無効な日付と見なされる)。Calendar とそのサブクラスとは異なり、Date and Time API では暦に関するバリデーションを厳格に行います

この他にも、異なるローカル暦の日付の直接比較は基本的にサポートされないと考えた方が無難です。日付比較は ISO 8601 の日付に変換した上で行うのが安全なやり方であり、実際に API ドキュメントでもそのように行うよう指示があります。

総じて、ローカル暦は ISO 8601 日付に比べて制約が多く、また暦によってその制約が異なることに注意してください。

Date and Time API の JapaneseDate は明治 6 年 1 月 1 日 (1873-01-01) 以降の日付が有効で、かつ元号と日付の対応が table 1 に従っていなければなりません。

table 1 - 和暦・西暦対応表
元号記号当該元号の日付対応する西暦日付
明治 M M01.01.01 (*1) 1868-01-25
M01.09.08 (*1) 1868-10-23
M05.12.02 (*2) 1872-12-31
M06.01.01 (*2) 1873-01-01
M45.07.29 1912-07-29
大正 T T01.07.30 1912-07-30
T15.12.24 1926-12-24
昭和 S S01.12.25 1926-12-25
S64.01.07 1989-01-07
平成 H H01.01.08 1989-01-08

(*1) 慶応 4 年 9 月 8 日 (西暦 1968 年 10 月 23 日) に改元が即日実施され、この日をもって明治元年 9 月 8 日となりました。また、この改元は慶応 4 年 1 月 1 日 (西暦 1868 年 1 月 25 日) に遡って行われました。

(*2) 西暦 1873 年 1 月 1 日に合わせて旧暦 (天保暦) から西暦 (グレゴリオ暦) への移行が実施されました。前日の西暦 1872 年 12 月 31 日が旧暦の明治 5 年 12 月 2 日であったことから、12 月 3 日~12 月 31 日までをスキップして、明治 6 年 1 月 1 日 = 西暦 1873 年 1 月 1 日となるように改正されました。

参考: ISO 8601 -- Date and Time API の基礎知識

なお、誤りの選択肢は以下のような日付となります。

(1) 1872-12-02 → 1872 年 = 明治 5 年、12 月 2 日 = 明治 5 年の最後の日付、から合成

(2) 1872-12-31 → 明治 6 年 1 月 1 日の前日の西暦換算

(3) 1873-01-01 → 明治 6 年 1 月 1 日 (行き止まりになると思った?)

教訓

  • Date and Time API のフィールドのバリデーションは厳格。特にローカル暦を使用する場合には各フィールドの定義域に注意すること。
  • 日本が旧暦 (天保暦) から西暦 (グレゴリオ暦) に移行したのは明治 6 年 (1873 年) で、明治 5 年までの日付は ISO 8601 とは互換性がない※一般常識としてこのくらいは知っておけ!
  • 和暦を使用するのであれば、各元号の開始日と終了日について完全に把握しておくこと。それができないようであれば、和暦を使用すべきではない。
  • Date and Time API を使うときは、Calendar の動作は一旦忘れること。旧 API はバリデーションがデタラメで、思わぬバグを引き起こす。

蛇足 (1) -- みかしーの誕生日は昭和?平成?

みかしー (三上枝織さん) の誕生日は 1989 年 1 月 6 日、つまり、ぎりぎり昭和 64 年生まれです (平成生まれではないのです)。では、Date and Time API がみかしーの誕生日を正しく和暦で表せるか、確かめてみましょう。これは OpenJDK 9 の JShell があれば簡単に行えます。

System.out.println(JapaneseDate.from(LocalDate.of(1989, 1, 6)));

実行結果は以下の通りです。

Japanese Showa 64-01-06

確かに、みかしーが昭和 64 年生まれだとわかります。

蛇足 (2) -- 平成 100 年問題について

JIS X 0301 における規定では、改元後もシステムが対応するまでの間は古い元号を継続して使用してもよいことになっています。そのために発生しうるのが「昭和 100 年問題」です。Date and Time API の現在の実装では、平成の終わりが定められていないことから、仮に今上天皇陛下が皇太子殿下に譲位することになっても、当面の間はシステム上で平成という元号を使い続けることは可能になっています。そこで現れるのが「平成 100 年問題」です。

和暦は年のフィールドを 2 桁で保持するため、移行措置に甘えてシステム改修を怠ると年フィールドのオーバーフローを起こすことになります。Date and Time API のバリデーションは強力のため、年フィールドのオーバーフロー時には DateTimeException がスローされるはずです。後々面倒なことになるため、日付処理は ISO 8601 で統一するのが一番の安全策です。

官公庁のように和暦しか使わないところであっても、強行採決による法改正や抵抗する公務員のレイオフを徹底的に行い、国際標準かつ副作用の少ない ISO 8601 に統一するのが今後「電子政府」化を推し進めていく上で重要なことなのではないでしょうか?

ThreeTen Extra と ThreeTen Backport

この記事は Java Advent Calendar 2016 の 20 日目です。昨日はひらおかゆみ (@yumix_h) の「自作のJavaMailラッパーでiCloudメールにアクセスする」です。

本日は、Date and Time API の拡張ライブラリである ThreeTen Extra と Java SE 6/7 向け互換ライブラリである ThreeTen Backport についてご紹介します。

1. JSR 310 と 3 つの成果物

JSR 310 : Date and Time API は、java.util.Date と java.util.Calendar を代替する API を提供するというものでした。スペック・リードは Stephen Colebourne (Joda-Time 開発者) と Michael Nascimento Santos という 2 人の Java Champion が務めました。ベンダー (特に Oracle) の開発者がスペック・リードを務める JSR が多い中で、JSR 310 は数少ないコミュニティ主体の JSR だったのも特徴的です。

JSR 310 は OpenJDK 8 入りが決まってから Oracle の Roger Riggs が 3 人目のスペック・リードとして加わり、現在は Date and Time API のメンテナンス・リードを務めています。

JSR 310 の基本概念は JavaOne 2008 のセッションの時点でほぼ固まっており (SourceForge に当時の資料がアップロードされています)、そこでは 4 つの特徴―(1) 日付と時刻の包括的なモデル、(2) タイプ・セーフ (プリミティブ型を極力避け、読み書きしやすく)、(3) 既存クラスとの連携 (既存の Date を JSR 310 に合わせて改修)、(4) XML と JDBC の考慮―を掲げ、また人間向け表現として ISO 8601 を採用することが明言されました。

Early Draft Review の頃は Java SE 6 で実装され、当初は Java SE 7 を目標に掲げていました。しかし、Early Draft Review の段階でクラス数が 102 というとち狂った非常に大きな仕様だったため、OpenJDK 8 へのマージに先だって大胆なダイエットを敢行し、Early Draft Review 2 でクラス数を 75 まで削減、Public Review ではクラス数を 69 にまで絞り込みました。これに小さな改修を加えてリリースしたものが Java SE 8 の Date and Time API です。

Early Draft Review 2 で JSR 310 の仕様から除外した UTC/TAI 対応 (うるう秒の考慮)、日付フィールド、暦 (ユリウス暦とコプト暦) などの機能を再構成し、ユーティリティといくつかの暦を追加したものが ThreeTen Extra です。Date and Time API と ThreeTen Extra を合わせると、JSR 310 が当初目指した姿に近いものとなります。

JSR 310 は Java SE 7 が目標だったことから、完成に近い段階まで Java SE 7 ベースで実装していました。OpenJDK 8 の Mercurial リポジトリに移ってからインタフェースのデフォルト実装の利用やラムダ式への対応がなされましたが、プロジェクトの GitHub リポジトリに残された実装を Java SE 8 の Date and Time API とほぼ同じ使い勝手となるように改修したものが ThreeTen Backport です。正式リリース直前に Java SE 6 へのバックポートが行われています。

まとめると、JSR 310 は 3 つの成果物を生みました。

  • Date and Time API -- Java SE 8 以降の標準 API
  • ThreeTen Extra -- Date and Time API から除外された機能 + α
  • ThreeTen Backport -- Java SE 6/7 向けの Date and Time API 互換ライブラリ

2. ThreeTen Extra

2.1. ThreeTen Extra のインストール

Maven Central に登録されています。Java SE 8 以降の他に依存するものはありません。

<dependency>
  <groupId>org.threeten</groupId>
  <artifactId>threeten-extra</artifactId>
  <version>1.0</version>
</dependency>

2.2. ThreeTen Extra の機能

ThreeTen Extra の機能はいくつかのカテゴリに分類することができます。

TAI/UTC 対応

JSR 310 の Early Draft Review 2 で「凝り過ぎた」として真っ先に削除された TAI/UTC 対応 (いわゆるうるう秒の考慮) がなされています。

  • TimeSource - UTC/TAI にも対応した Clock のためのインタフェース
  • TaiInstant - 国際原子時 (TAI) に準拠した時刻のものさし (Instant)
  • UtcInstant - 協定世界時 (UTC) に準拠した時刻のものさし (Instant)
  • UtcRules - うるう秒の管理、Instant / TaiInstant / UtcInstant の相互変換などを提供する

現在 Java が動作するプラットフォームのシステムクロックは、これらを十分に生かせるほどの精度を持ち合わせていませんが、高精度な外部クロックと接続できれば活用の機会はあるかと思われます。

日付・時刻要素

JSR 310 は日付・時刻の要素をプリミティブ型ではなくクラスで持たせて、バリデーションを厳格にする方針を採っていました。しかし、クラス数超過のため Early Draft Review 2 の段階で現在の日付・時刻クラスと YearYearMonthMonthDayMonthDayOfWeek に絞り込まざるを得ませんでした。ThreeTen Extra では JSR 310 から削除された日付・時刻の要素を復活させる形で実装しています。

  • YearQuarter - 年 + 四半期
  • YearWeek - 年の週; これに日が加わると暦週日付 (week date) となる
  • DayOfMonth - 月の日; Date and Time API では int として扱っている
  • DayOfYear - 年の日 = 年間通算日 (ordinal date)
  • AmPm - 午前 / 午後
  • Quarter - 四半期
  • PackedFields - ISO 8601 標準形式 (区切り記号を使わない) の日付・時刻フィールド

時間量

Date and Time API では時間量を PeriodDuration の 2 クラスで表現しますが、元々の JSR 310 ではより直感的な時間量を表すクラスも用意されていました。こちらも Early Draft Review 2 で一旦削除されたものがほぼそのままの形で復活したものです。

ローカル暦

JSR 310 から除外されたユリウス暦とコプト暦の他に、多数のローカル暦が追加されています。ディスコルディア暦とパックス暦は早い時期に実装されていますが、その他のローカル暦は後発の部類となります。

ユーティリティ・クラス

3. ThreeTen Backport

3.1. ThreeTen Backport のインストール

Maven Central に登録されています。Java SE 6 以降の他に依存するものはありません。

<dependency>
  <groupId>org.threeten</groupId>
  <artifactId>threetenbp</artifactId>
  <version>1.3.2</version>
</dependency>

3.2. ThreeTen Backport の機能

ThreeTen Backport は可能な限り Date and Time API と同じ振る舞いをするように考慮されています。Javadoc は http://www.threeten.org/threetenbp/apidocs/ にありますが、Date and Time API とほぼ同様であることが見て取れます。

Date and Time API からの相違点は以下の通りです。

  • パッケージ名 -- java.time ではなく org.threeten.bp
  • Java SE 8 を模倣するためのクラスを追加 -- org.threeten.bp.jdk8 パッケージ (4 クラス)
  • tz database 周りを独自に実装 -- org.threeten.bp.zone.TzdbZoneRulesProvider クラスおよび tzdb のデータファイル (tzdb は JDK ではなく ThreeTen Backport のバージョンアップ時に更新されるので注意)
  • デフォルト実装や static メソッドを持つインタフェースについて、抽象クラスに置き換え (暦周りを中心に)
  • org.threeten.bp.DateTimeUtils -- java.util.Date や java.util.Calendar などとの相互変換ユーティリティ

DateTimeUtils は本家 Date and Time API にはない、ThreeTen Backport 独自のユーティリティ・クラスです。Date and Time API と java.util.Date などの相互変換メソッドを作成する際には、DateTimeUtils の実装を参考にすると良いかもしれません。

明日は rysh(r.ishibashi) さん (@cactaceae) です。

Date and Time API 相互変換チャート

※この記事は、以下の過去ブログ記事の再掲です。

Date and Time API の主要な日付・時刻クラスについて、相互変換を図にしてみました。これは昨年 3 月の JJUG Java 8 ローンチ・イベントの発表資料に収録した図を、印刷に耐えられるよう改版したものです (マスタを Illustrator 形式で作成)。

イメージは以下の通りです。

jsr310-conversions.png

PDF 形式も用意しました → jsr310-conversions.pdf

この図を持っていても皆様の給料は上がりませんが、Date and Time API の使い勝手は少しだけ上がります。

ISO 8601 -- Date and Time API の基礎知識

今回は、Date and Time API の礎となっている ISO 8601 についてご紹介します。ISO 8601 の知識があると、クラス数からは複雑そうに感じる Date and Time API も容易に理解できるようになります。Date and Time API を習得する上でも、システム間連携で日付・時刻のやり取りをする場合にも、ISO 8601 の知識は必須となってきますので、ぜひ基本事項を押さえておいてください。

なお、この記事は、旧ブログの下記の記事を再編集したものです。

1. ISO 8601 とは?

ISO 8601 は "Data elementsand interchange formats--Information interchange--Representation of dates and times" (和訳: 情報交換のためのデータ要素及び交換方式ー日付及び時刻の表記) というタイトルが表すように、コンピュータ間で日付と時刻の送受信を行うためのフォーマットを規定する規格です。ISO 8601 以前にも日付と時刻の表現を定めた規格は複数存在していましたが、ISO 8601 はそれらを統合した規格となっています。現在主に使用されているものは第2版の ISO 8601:2000 です。

日本工業規格 (JIS) では、ISO 8601 の邦訳に日本独自の「元号」に関する記述を付加した JIS X 0301 (現行版は JIS X 0301:2002) を規格化しています。記述のほとんどは ISO 8601 そのものであり、また原文との差分も明確に表現しているため、JIS X 0301ISO 8601 の実質的な邦訳と見なして問題ないでしょう。

ISO 8601 の規格書は本稿執筆時点で 138.00 CHF と高価です。一方、JIS X 0301JISC (日本工業標準調査会) の Web サイトで無料で閲覧できるほか、規格書自体も 2,000 円 (税抜) と入手しやすくなっています (冊子と PDF を選択できますが、PDF 版は紙媒体のスキャンデータのため、冊子の方が扱いやすいでしょう)。

JIS X 0301 に限らず、JIS 規格書は日本規格協会から購入するか、最寄りの書店で取り寄せてもらうことが出来ます。なお、下記書店では全 JIS 規格書を直接取り扱っていますので、在庫があればすぐに入手できます。

ISO 8601 は、西暦 = グレゴリオ暦 (Gregorian calendar) による年月日で表す日付、時刻および期間の表し方について規定するものです。具体的には以下のものについて規定します。

  1. 年月日を用いて表す日付 (暦日付; calendar date)
  2. 年および年日 (年初からの通算日) を用いて表す日付 (年間通算日; ordinal date)
  3. 年、週および週日 (曜日) を用いて表す日付 (暦週日付; week date)
  4. 1 日 24 時間制に基づいて表す時刻
  5. 日付と時刻の組み合わせ
  6. 地方時 (local time) と協定世界時 (UTC)
  7. 時間間隔 (period of time, time-interval)
  8. 反復時間間隔 (recurring time-interval)

また、上記規定に必要な各種用語や概念についても定義しています (一部は他の規格を参照する形となります)。

ISO 8601 はコンピュータが日付・時刻を扱うための規格であるため、私たちが日常使用している暦をベースとしながらも、電算処理上の合理性を満たすように設計されています。いくつかの例を以下に示します。

  • 日付の年月日の順序は、欧米では英国式の Day Month Year 順と米国式の Month Day Year 順があります。電算処理上は統一する必要があるため、日常表記と混ざらないように英国式・米国式のいずれでもない Year Month Day 順とし、区切り文字も日常的に使われるスペース (' ')、スラッシュ ('/')、ピリオド ('.')、カンマ (',') を避け、ハイフン ('-') としました (しかし、日本における日付表現が Year Month Day 順でかつスラッシュ区切りを常用していることは運が悪かったとしか言い様がありません)。
  • 時刻の時分秒の順序は各国とも Hour Minute Second 順でブレがないためそのまま採用しています。ただし時刻は 24 時間制に統一しています。
  • 電算処理の都合上、区切り文字は省略可能とし、また日付の年月日、時刻の時分秒はそれぞれ桁数が固定となっています。日本においては '2014/3/21' のような表記が日常的に使用されますが、ISO 8601 では区切り文字ではなく桁数を基準に日付・時刻の要素 (フィールド) を抽出することに注意してください (ここは Date and Time API のformat/parseで多くの人がハマるポイントでもあります)。

インターネットのタイムスタンプは、伝統的に RFC 2822 (旧 RFC 822) に則った形式が用いられていましたが、2002 年に ISO 8601 を簡略化した RFC 3339 が制定され、W3C XML Schema の DateTime 型などに採用されています。

2. 日付の表現

第 1 章で紹介した通り、ISO 8601 の日付表現には暦日付 (calendar date)、年間通算日 (ordinal date)、暦週日付 (week date) の 3 種類あります。

2.1. 暦日付 (calendar date)

書式 (拡張形式): YYYY-MM-DD (標準形式: YYYYMMDD)

  • 例 (拡張形式): 2014-03-21 (標準形式: 20140321)

以下共通ですが、ISO 8601 の日付・時刻表現のほとんどには区切り文字を含む拡張形式と含まない標準形式があります。Date and Time API が文字列表現として採用しているのは主に拡張形式です。

YYYY は年を表し、必ず数字 4 桁で、0000 ~ 9999 までの範囲となります。ただし、グレゴリオ暦が制定されたのが 1582 年 10 月であるため、0000 ~ 1582 は当事者間の合意がある場合のみ使用します (= 事前の取り決めがなければ使用してはいけません)。なお、0001 年は西暦 1 年を表すため、0000 年はその前年である紀元前 1 年を意味します。

MM は月を表し、必ず数字 2 桁で、01 ~ 12 までの範囲となります。DD は日を表し、必ず数字 2 桁です。日の最小値は 01 ですが、最大値は下表に示す通り月と閏年によって 28、29、30 または 31 のいずれかとなります。

月と日の対応
010203040506070809101112
平年 31 28 31 30 31 30 31 31 30 31 30 31
閏年 31 29 31 30 31 30 31 31 30 31 30 31

閏年はグレゴリオ暦において 1 年が 366 日となる年をいい、閏年でない年を平年と呼びます。閏年の判定は下表に示すよく知られた方法があります。

平年と閏年の判定
年 mod 4年 mod 100年 mod 400判定
0 以外 N/A N/A 平年
0 0 以外 N/A 閏年
0 0 0 以外 平年
0 0 0 閏年

余談ですが、グレゴリオ暦より以前に用いられていたユリウス暦にも閏年はありましたが、常に 4 年に 1 度設定され、グレゴリオ暦のように「100 年に一度は平年」「400 年に一度は閏年」といった補正はありませんでした。

ユリウス暦は細かな修正を行いながらも 1,000 以上の長きに渡って使用されてきましたが、ユリウス暦の春分の日と観測結果による春分の乖離が無視できない状況になってきました。春分はキリスト教において復活祭の日程を決める基準となるもので、この乖離はキリスト教としては許容できないものでした。そこで 16 世紀に当時のローマ教皇グレゴリウスの指示により、多数の天文学者を招聘してユリウス暦の徹底的な改修を実施し、その結果生まれたのがグレゴリオ暦です。グレゴリオ暦は 1582 年 10 月から施行されており、その直前にはユリウス暦からグレゴリオ暦に移行するための日付のスキップが行われています。なお、グレゴリオ暦の導入は国ごとに異なり、明治維新直後に真っ先に取り入れた日本のような国もあれば、イギリスのように後年になってから切り替えを行った国もあります。

Date and Time API では、閏年判定を行うユーティリティメソッドは時差なし日付を表す LocalDate と年のみを表す Year に存在しますので、これを用いると便利です。

【補足 1: 拡大表記】

ISO 8601 では、年は必ず 4 桁固定となりますが、当事者間の合意があれば 4 桁よりも大きい桁数やマイナスの値を使用する「拡大表記」が認められています (ただし、桁数は固定する必要があります)。ただし、拡大表記を用いると他システム連携で問題が発生する場合があり得るため、可能な限り ISO 8601 の規定に従うことをお勧めします。

Date and Time API では拡大表記に基づき、年を -999,999,999~999,999,999 の範囲で指定することが可能です。

【補足 2: 13 の月の暦への対応】

ISO 8601 では月は必ず 01~12 の範囲となります。ただし、Date and Time API は ISO 8601 以外の暦にも対応可能な拡張ポイントを持っており、例えば 1 年が 13 ヶ月で構成されるようなローカルな暦 (当然、ISO 8601 には従っていない) にも対応可能になっています。本記事の記載はあくまで ISO 8601 に準拠した場合のものとしてご理解ください。

2.2. 年間通算日 (ordinal date)

書式 (拡張形式): YYYY-DDD (標準形式: YYYYDDD)

  • 例 (拡張形式): 2014-080 (標準形式: 2014080)

YYYY は暦日付同様に年を表し、必ず数字 4 桁で、0000 ~ 9999 までの範囲となります。取り得る値に関する諸注意や拡大表記に関しても同じ規則が適用されます。

DDD は年初からの通し日となり、必ず数字 3 桁となります。範囲は年初日である 1 月 1 日が 001、年末日である 12 月31 日が 365 (平年) または 366 (閏年) となります (*3)。

年間通算日では「月」は関係ありませんが、年初からの通し日と各月の日数から下表のような対応になります。

月と通し日の対応表
月数 (*1)月名月内の日数 (*2)平年の通し日 (*3)閏年の通し日 (*3)
01 1 月 (January) 01-31 001-031 001-031
02 2 月 (February) 01-28 (閏年: 29) 032-059 032-060
03 3 月 (March) 01-31 060-090 061-091
04 4 月 (April) 01-30 091-120 092-121
05 5 月 (May) 01-31 121-151 122-152
06 6 月 (June) 01-30 152-181 153-182
07 7 月 (July) 01-31 182-212 183-213
08 8 月 (August) 01-31 213-243 214-244
09 9 月 (September) 01-30 244-273 245-273
10 10 月 (October) 01-31 274-304 275-305
11 11 月 (November) 01-30 305-334 306-335
12 12 月 (December) 01-31 335-365 336-366

(*1) 暦日付における MM の定義域と一致する。

(*2) 暦日付における DD の定義域と一致する。

(*3) 年間通算日における DDD の定義域と一致する。

さて、暦日付と年間通算日の判別ですが、原則としてフィールドおよび全体の桁数で行います。ISO 8601 は桁数を厳密に規定しているため、このやり方で日付文字列のパース時に暦日付と年間通算日を容易に判別することが可能です。

  • 暦日付: 年 4 桁 + 月 2 桁 + 日 2 桁 = 8 桁
  • 年間通算日: 年 4 桁 + 通し日 3 桁 = 7 桁

Date and Time API では、年間通算日を扱うのに、

の組み合わせを用います。

2.3. 暦週日付

書式 (拡張形式): YYYY-Www-D (標準形式: YYYYWwwD)

  • 例 (拡張形式): 2014-W12-5 (標準形式: YYYYW125)

日付の基本表現の 3 つ目は、その年の週と曜日により指定するものです。ww は 2 桁固定で、その年の何週目か (01 ~ 52/53) を表し、D は ww 週の曜日を表す通算日 1~7 となります。指示記号 'W' は暦日付や年間通算日との識別のため省略はできません。

ISO 8601 に置いては、曜日は以下のように定義されています。

ISO 8601 における曜日の定義
通算日1234567
曜日 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日

また、週と曜日については 4 つの取り決めごとがあります。

  • すべての週について、その始まりは「月曜日」である。
  • ある年の最初の週はその年の最初の木曜日を含む週である。
  • ある年の最後の週は、次の年の最初の週の 1 つ前の週である。
  • 2000 年 1 月 1 日は「土曜日」と定める。

上記の取り決めから、週については次の定理が成り立ちます。

  • 1 年は 52 週または 53 週からなる。
  • ある年の 1 月 4 日を含む週がその年の最初の週である。
  • 週は年をまたいで定義される場合があるため、暦週日付の合計は、暦日付または年間通算日の合計とは一致しません。例えば以下の通り:
    • 1995 年 1 月 1 日 (日) は、1994 年の 52 番目の週の 7 番目の日である。
    • 1996 年 12 月 31 日 (火) は、1997 年の 1 番目の週の 2 番目の日である。

なお、週は各地の文化により解釈が異なるため、ISO 8601 ではあまり使用しないことを推奨しています。

Date and Time API では 3 種類の日付表現をすべてサポートしますが、使い勝手の良いインタフェースを提供しているのは暦日付です。暦週日付は文字列表現もしくはいくつかの日付演算の組み合わせを用いて実現します。

2.4. 下位省略表記

暦日付と暦週日付については、精度が要求されない場合には「月」「日」または「曜日」を省略することができます。

書式 1 (拡張形式): YYYY-MM

  • 例 1 (拡張形式): 2014-03

書式 2 (拡張形式): YYYY-Www (基本形式: YYYYWww)

  • 例 2 (拡張形式): 2014-W12 (基本形式: 2014W12)

書式 3 (基本形式): YYYY

  • 例 3 (基本形式): 2014

書式 4 (基本形式): YY

  • 例 4 (基本形式): 20

書式 2 は後述の上位省略表記との重複を避けるため、また書式 3 および書式 4 には区切り文字を入れる場所がないため、それぞれ拡張形式は用意されていません。さらに書式 4 は世紀 (2001 年~2100 年) ではなく百年台 (2000 年~2099 年) を表すことに注意してください。

Date and Time API では、YearMonth クラスや Year クラスが下位省略表記を表します (百年台に対応するクラスはありません)。

なお、年間通算日には下位省略表記は用意されていません。

2.5. 上位省略表記

当事者間の合意が取れているの場合は、「年」の一部または全部、「月」を省略することができます。私たちが日常使用している表現に近いものである一方、「年」の一部を省略するもの (書式 5 から書式7) は「西暦 2000 年問題」を抱え続けているため注意が必要です。

【注意】各例における「現在の日付」は、本記事執筆時点の 2015 年 5 月 dd 日とします。

(暦日付の上位省略表記)

書式 1 (拡張形式): YY-MM-DD (基本形式: YYMMDD)

  • 例 1 (基本形式): 14-03-21 (基本形式: 140321) [2014-03-21 の上位省略表記]

書式 2 (拡張形式): YY-MM (基本形式: YYMM)

  • 例 2 (基本形式): 14-03 (基本形式: 1403) [2014-03 の上位省略表記]

書式 3 (基本形式): YY

  • 例 3 (基本形式): 14 [2014 の上位省略表記]

書式 4 (拡張形式) --MM-DD (基本形式: --MMDD)

  • 例 4 (拡張形式) --03-21 (基本形式: --0321) [2015-03-21 の上位省略表記]

書式 5 (基本形式) --MM

  • 例 5 (基本形式) --03 [2015-03 の上位省略表記]

書式 6 (基本形式) ---DD

  • 例 6 (基本形式) ---21 [2015-03-21 の上位省略表記]

暦日付の上位省略表記で用いるハイフンのうち、先頭に付加されるものはむしろ指示記号に近いものとして用いられます (これは年間通算日および暦週日付も同様です)。

Date and Time API は、書式 4 に対応する MonthDay と書式 5 に対応する Month を用意しています。また ThreeTen Extra には書式 6 に対応する DayOfMonth が含まれます。

(年間通算日の上位省略表記)

書式 7 (拡張形式): YY-DDD (基本形式: YYDDD)

  • 例 7 (拡張形式): 14-080 (基本形式: 14080) [2014-080 の上位省略表記]

書式 8 (基本形式): -DDD

  • 例 8 (基本形式): -080 [2015-080 の上位省略表記]

年間通算日の上位省略表記は書式 7 のみ拡張形式があります。

Date and Time API には年間通算日年間通算日の上位省略表記に対応するクラスは用意されていませんが、ThreeTen Extra には対応する DayOfYear クラスが含まれています。

(暦週日付の上位省略表記)

書式 9 (拡張形式): YY-Www-D (基本形式: YYWwwD)

  • 例 9 (拡張形式): 14-W12-5 (基本形式: 14W125) [2014-W12-5 の上位省略表記]

書式 10 (拡張形式): YY-Www (基本形式: YYWww)

  • 例 10 (拡張形式): 14-W12 (基本形式: 14W12) [2014-W12 の上位省略表記]

書式 11 (拡張形式): Y-Www-D (基本形式: YWwwD)

  • 例 11 (拡張形式): 4-W12-5 (基本形式: 4W125) [2014-W12-5 の上位省略表記]

書式 12 (拡張形式): Y-Www (基本形式: YWww)

  • 例 12 (拡張形式): 4-W12 (基本形式 4W12) [2014-W12 の上位省略表記]

書式 13 (拡張形式): -Www-D (基本形式 -WwwD)

  • 例 13 (拡張形式): -W12-5 (基本形式 -W125) [2015-W12-5 の上位省略表記]

書式 14 (基本形式): -Www

  • 例 14 (基本形式): -W12 [2015-W12 の上位省略表記]

書式 15 (基本形式): -W-D

  • 例 15 (基本形式): -W-5 [2015-Wdd-5 の上位省略表記]

Date and Time API には暦週日付の上位省略表記に対応するクラスは用意されていません。

3. 時刻の表現

3.1. 時刻

書式 1 (拡張形式): hh:mm:ss (基本形式: hhmmss)

  • 例 1-1 (拡張形式): 00:00:00 (基本形式: 000000) [1 日の始まり]
  • 例 1-2 (拡張形式): 09:30:45 (基本形式: 093045)
  • 例 1-3 (拡張形式): 12:00:00 (基本形式: 120000) [正午]
  • 例 1-4 (拡張形式): 15:30:00 (基本形式: 153000)
  • 例 1-5 (拡張形式): 21:00:00 (基本形式: 210000)
  • 例 1-6 (拡張形式): 23:59:59 (基本形式: 235959)
  • 例 1-7 (拡張形式): 23:59:60 (基本形式: 235960) [正の閏秒]
  • 例 1-8 (拡張形式): 24:00:00 (基本形式: 240000) [1 日の終わり] = 翌日の 00:00:00 (000000) と同じ

hh の範囲は 00-24、mm の範囲は 00-59、ss の範囲は 00-60 と定められています。ただし、hh = 24 は 24:00:00 (= 翌日 00:00:00) の場合のみ使用できます。また、ss = 60 は正の閏秒が挿入されたときのみ発生しうる値のため、ss の範囲は通常 00-59 の間となります。

Date and Time API では 24:00:00 の表記はサポートされません。必ず翌日の 00:00:00 として表現する必要があります。

3.2. 時刻の下位省略表記と上位省略表記

(下位省略表記)

書式 1 (拡張形式): hh:mm (基本形式: hhmm)

  • 例 1-1 (拡張形式): 00:00 (基本形式 0000)
  • 例 1-2 (拡張形式): 12:00 (基本形式 1200)
  • 例 1-3 (拡張形式): 15:30 (基本形式 1530)

書式 2 (基本形式): hh

  • 例 2-1 (基本形式): 21

(上位省略表記)

書式 3 (拡張形式) -mm:ss (基本形式: -mmss)

  • 例 3-1 (拡張形式) -00:00 (基本形式: -0000) [hh:30:00 (hh; 00-24) の上位省略表記]
  • 例 3-2 (拡張形式) -30:00 (基本形式: -3000) [hh:30:00 (hh; 00-23) の上位省略表記]
  • 例 3-3 (拡張形式) -30:45 (基本形式: -3045) [hh:30:00 (hh; 00-23) の上位省略表記]

書式 4 (基本形式) -mm

  • 例 4-1 (基本形式) -00 [hh:00 (hh; 00-24) の上位省略表記]
  • 例 4-2 (基本形式) -30 [hh:30 (hh; 00-23) の上位省略表記]

書式 5 (標準形式) --ss

  1. 例 5-1 (基本形式) --00 [hh:mm:00 (hh; 00-24, mm; 00-59) の上位省略表記]
  2. 例 5-2 (基本形式) --45 [hh:mm:45 (hh; 00-23, mm; 00-59) の上位省略表記]

3.3. 小数点を含む時刻

書式 1 (拡張形式): hh:mm:ss.ss (標準形式: hhmmss.ss) [s; 0 文字以上の数字]

  • 例 1-1 (拡張形式): 15:30:45.250 (標準形式: 153045.250)
  • 例 1-2 (拡張形式): 18:15:00.5 (標準形式: 181500.5)

秒未満 (ミリ秒、マイクロ秒など) の精度が必要であれば、以下の条件の下で秒の小数点表記を使用することができます。

  • 当事者間の合意に基づき小数点以下の桁数 (固定長) が決定されていること
  • 整数部と小数部はピリオド '.' または カンマ ',' で区切られていること
  • 1 秒未満は必ず整数部を '00' とすること

Date and Time API では、時刻を小数部桁数が 9 桁 (すなわちナノ秒の精度) で扱います。LocalTime.toString() メソッドが返す文字列表現は、hh:mm (下位省略表記)、hh:mm:ss (小数部桁数なし)、小数部桁数 3 桁、6 桁および 9 桁の 5 段階のうち、LocalTime が保持している値を表現するのに最小限必要なものが自動的に選択されます。

書式 2 (拡張形式): hh:mm.mm (標準形式: hhmm.mm) [m; 0 文字以上の数字]

  1. 例 2 (拡張形式): 14:00.25 (標準形式: 1400.25) [14:00:15 と同義]

書式 3 (標準形式): hh.hh [h; 0 文字以上の数字]

  • 例3 (標準形式): 12.5 [12:30:00 と同義]

下位省略表記により「秒」または「分」を省略している場合は、それぞれ「分」または「時」の小数点表記を用いることができます。

Date and Time API は、標準では「分」および「時」の小数点表記をサポートしていません。

4. 日付と時刻の組み合わせ

書式 1: 暦日付 'T' 時刻

  • 例1 (拡張形式): 2014-03-21T13:00:00 (標準形式: 20140321T130000)

書式 2: 年間通算日 'T' 時刻

  • 例2 (拡張形式): 2014-080T13:00 (標準形式: 2014080T1300)

書式 3: 暦週日付 'T' 時刻

  • 例 3 (拡張形式): 2014-W12-5T13:00:00.250 (標準形式: 2014W125T130000.250)

日付と時刻は組み合わせて表記することが可能です。ただし下記のルールに従う必要があります。

  • 日付と時刻の間は指示記号 'T' で区切る (当事者間の合意があれば省略も可能だが、'T' 以外の文字、例えば空白文字を使用することは許されない)
  • 日付は「暦日付」「年間通算日」「暦週日付」から任意に選択可能。上位省略表記を使用することは可能だが、下位省略表記は使用できない。
  • 時刻は下位省略表記または小数点表記を使用することが可能である。ただし、上位省略表記は使用できない。
  • 標準形式と拡張形式のいずれを使用しても良いが、両者を混在させてはならず、必ずどちらかに統一する。

ISO 8601 では、適用業務が日付のみを扱うのか、あるいは時刻のみを扱うのか明確でない場合には (本当はこのような曖昧な仕様ではいけないのですが...)、日付と時刻の組み合わせとして扱うように取り決めています。

5. 地方時と協定世界時

5.1. 地方時と協定世界時

地球はおよそ 24 時間で自転しています。これが意味することは、地球の位置関係が同一の瞬間であっても、特定の場所では昼 (太陽に面している) であるが、また別の場所では夜 (太陽から隠れている) ことを意味します。このことはまた、観測地点によって時刻がまちまちであることをも示します。観測地点における時刻を「地方時 (local time)」と呼びます。

さて、地方時のうち世界中時刻の基準となっているものがあります。現在「協定世界時 (UTC)」と呼ばれているものがそれで、本初子午線 (経度 0°) における時刻を表します。本初子午線における時刻には歴史的に「世界時 (UT)」や「グリニッジ標準時 (GMT)」も使われていました。これらは計測方法により若干の誤差はありますが、概ね同じ時刻を表します。

現在の本初子午線であるグリニッジ子午線は、イギリスのグリニッジ天文台が存在する場所の経度を指しています。しかし、歴史的には本初子午線の決定が先で、グリニッジ天文台は本初子午線のランドマークとして建設された天文台だったようです。なお、グリニッジ子午線が国際的な本初子午線として認められたときは、フランスはその決定に不服で、決定から 1 世紀ほど後になるまでフランス独自の本初子午線を使用してとのことです。

GMT は本初子午線における太陽平均時 (天体観測の結果から太陽の動きを逆算して時刻を求める方法で、太陽を直接観測して得られる時刻より高精度です) を基準としています。GMT はあくまで本初子午線における太陽平均時による時刻であるため、観測地点が異なると同じ計測方法でも差異が出ます。そこで、より汎用的な Universal Time (UT) が導入されました。UT には補正の度合いに応じて UT0、UT1、UT2 の 3 種類があり、UT0 は観測地点における太陽平均時 (本初子午線で観測した場合は GMT と同じ)、UT1 は複数の UT0 を平均したものです。通常 UT と行った場合には UT1 を指します。UT2 は UT1 に対して地球の動きを加味した補正を行ったもので最も高精度となります。

UTC は 1972 年以降に導入されたもので、基準を国際原子時 (TAI) として、TAI と UT1 の差分が 0.9 秒以内になるよう時々うるう秒を挿入します。現在はもっぱら UTC が基準時刻として使用されています。

UTC 導入の背景には、TAI があまりに高精度のため、天体観測から導き出した UT1 との差分か無視できなくなったことが挙げられます。TAI は 1950 年代に開始され、1958 年 1 月 1 日に UT2 (UT1 よりも高精度な表現) と同期を取っています。TAI は原子時計が刻む厳密な時刻が基準ですが、UT は天体観測が基準のため徐々に誤差が出始めています。近年、UTC に対する閏秒挿入が増加している背景には、このような理由があります。

5.2. 時差の表現方法

ISO 8601 では、協定世界時からの時差という形で地方時の時間帯を明確にする方法が規定されています。具体的には時刻 (上位省略表記を用いていない) または日付+時刻に時間帯を追加します

式 1 (拡張形式): ±hh:mm (基本形式: ±hhmm) -- UTC からの時差 (分までの単位)

  • 例 1-1 (拡張形式): 2014-03-21T13:00+09:00 (基本形式: 20140321T13:00+0900)
  • 例 1-2 (拡張形式): 20:00:00-08:00 (基本形式: 20:00:00-0800)

書式 2 (標準形式): ±hh -- UTC からの時差がちょうど 1 時間単位の場合の省略表記

  • 例 2-1 (標準形式): 2014-03-21T13:00+09 [+0900 と同義]
  • 例 2-2 (標準形式): 20:00:00-08 [-0800 と同義]

書式 3: 協定世界時の場合は指示記号 'Z'

  • 例 3: 2014-03-21T04:00:00Z

6. 時間の表し方

6.1. 時点

長さ 0 とみなされる時間軸上の点を時点 (time-point) といい、時間軸上の位置 (0 点からの距離) によって示します。

ISO 8601 では暦日付の基準として、国際メートル条約が調印された年を 1875 年と定めています。また、暦週日付の基準として、2000 年 1 月 1 日を土曜日と定めています。

Date and Time API では 0 点 (エポック) として 1970 年 1 月 1 日 0 時 0 分 0 秒 (GMT) を使用しています。これは Java が当初から使用してきた Unix 時刻と同じものです。

6.2. 時間長

連続した時間の長さを時間長 (duration) といいます。時間長は主として時間間隔または反復時間間隔を表現するために用いられます。

書式 1: nYnMnDTnHnMnS -- 年月日時分秒の量で指定する

  • 例 1: 2Y10M15DY10H20M30S [2 年 10 ヶ月 15 日間と 10 時間 20 分 30 秒]

書式 2: nW -- 週の数で指定する

  • 例 2: 10W [10 週間]

時間長には下位省略表記、小数点表記および上位省略表記があります。これらは以下のルールに従います。

  • 必要な場合には最下位の構成要素を省略できる。
  • 必要な場合には最下位の構成要素を小数点表記で表すことができる (要領は時刻の小数点表記と同様)。
  • 年、月、日、時、分、秒の数が 0 の場合には、その数と対応する指示記号を省略できる。ただし、すべて 0 の場合でも 1 つは必ず表記すること。
  • 時、分、秒がすべて省略された場合は、指示記号 'T' は省略することができる。

当事者間の合意があれば、時間長の表現として日付または時刻の表現を代用することができます。

Date and Time API では代用形式を使用することはできません。

6.3. 時間間隔

2 つの時点の間の時間部分を時間間隔 (period of time, time-interval)といいます。時間間隔を構成する時点は「始点」および「終点」として表します。時間間隔の書式は下記の 4 通りです。

書式 1 (拡張形式): YYYY-MM-DDThh:mm:ss/YYYY-MM-DDThh:mm:ss (基本形式: YYYYMMDDThhmmss/YYYYMMDDThhmmss) -- 始点 / 終点 (始点と終点を指示記号 '/' で区切る)

  • 例 1: 2012-04-01T12:00:00/2014-03-21T13:30:00 [2012 年 4 月 1 日 12 時 0 分 0 秒から 2014 年 3 月 21 日 13 時 30 分 0 秒の間の時間間隔]

書式 2 (基本形式): PnYnMnDTnHnMnS -- 時間長

  • 例 2: P1Y9M15DT1H30M0S [1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒の時間間隔 ※時間軸上の位置は特定されない]

書式 3 (拡張形式): YYYY-MM-DDThh:mm:ss/PnYnMnDTnHnMnS (基本形式: YYYYMMDDThhmmss/PnYnMnDTnHnMnS) -- 始点 / 時間長 (始点と時間長を指示記号 '/' で区切る)

  • 例 3: 2014-03-21T13:30:00/P1Y9M15DT1H30M0S [2014 年 3 月 21 日 13 時 30 分 0 秒から始まる、1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒の時間間隔]

書式 4 (拡張形式): PnYnMnDTnHnMnS/YYYY-MM-DDThh:mm:ss (基本形式: PnYnMnDTnHnMnS/YYYYMMDDThhmmss) -- 時間長 / 終点 (時間長と終点を指示記号 '/' で区切る)

  • 例 4: P1Y9M15DT1H30M0S/2014-03-21T13:30:00 [2014 年 3 月 21 日 13 時 30 分 0 秒で終わる、1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒の時間間隔]

始点および終点は、日付・時刻の完全表記を用います (基本形式または拡張形式)。時間長は指示記号 'P' を前置します。

6.4. 反復時間間隔

【重要】 Date and Time API は反復時間間隔をサポートしません。

同じ長さの時間長が切れ目なく続いた期間を反復時間間隔 (recurring time-interval) といいます。反復時間間隔は、時間間隔および反復回数によって指定します。反復時間間隔の具体的な書式は下記の 4 通りです。

書式 1 (拡張形式): Rn/YYYY-MM-DDThh:mm:ss/YYYY-MM-DDThh:mm:ss (基本形式: Rn/YYYYMMDDThhmmss/YYYYMMDDThhmmss) -- 反復回数 / 始点 / 終点 (反復回数、始点、終点をそれぞれ指示記号 '/' で区切る)

  • 例 1: R3/2012-04-01T12:00:00/2014-03-21T13:30:00 [2012 年 4 月 1 日 12 時 0 分 0 秒から 2014 年 3 月 21 日 13 時 30 分 0 秒の間と、それと同じ長さの時間を計 3 回繰り返した反復時間間隔]

書式 2 (基本形式): Rn/PnYnMnDTnHnMnS -- 反復回数 / 時間長 (反復回数と時間長を指示記号 '/' で区切る)

  • 例 2: R6/P1Y9M15DT1H30M0S [1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒を 6 回繰り返した反復時間間隔 ※時間軸上の位置は特定されない]

書式 3 (拡張形式): Rn/YYYY-MM-DDThh:mm:ss/PnYnMnDTnHnMnS (基本形式: Rn/YYYYMMDDThhmmss/PnYnMnDTnHnMnS) -- 反復回数 / 始点 / 時間長 (反復回数、始点、時間長をそれぞれ指示記号 '/' で区切る)

  • 例 3: R12/2014-03-21T13:30:00/P1Y9M15DT1H30M0S [2014 年 3 月 21 日 13 時 30 分 0 秒から始まる、1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒を計 12 回繰り返した反復時間間隔]

書式 4 (拡張形式): Rn/PnYnMnDTnHnMnS/YYYY-MM-DDThh:mm:ss (基本形式: Rn/PnYnMnDTnHnMnS/YYYYMMDDThhmmss) -- 反復回数 / 時間長 / 終点 (反復回数、時間長、終点をそれぞれ指示記号 '/' で区切る)

  • 例 4: R12/P1Y9M15DT1H30M0S/2014-03-21T13:30:00 [2014 年 3 月 21 日 13 時 30 分 0 秒で終わる、1 年 9 ヶ月 15 日間と 1 時間 30 分 0 秒を計 12 回繰り返した反復時間間隔]

反復回数は指示記号 'R' を前置します。反復回数が 1 の時は通常の時間間隔と同じ意味になります。反復回数は省略可能で、省略時には反復回数未定 (無限) となります。始点および終点は、日付・時刻の完全表記を用います (基本形式または拡張形式)。時間長は指示記号 'P' を前置します。

7. 年号の表現 (JIS X 0301)

Date and Time API の和暦対応は JIS X 0301 に準拠しておらず、DateTimeFormatter を用いてフォーマットをカスタマイズする必要があります。

ISO 8601 には規定されていませんが、JIS X 0301 ではローカライズに合わせて「元号」の取り扱いについても補足的に触れています。

JIS X 0301 では元号を使用する場合、暦日付を以下の書式で表現することができます。

書式 (拡張形式): NYY.MM.DD (基本形式: YY.MM.DD) [N: 元号を表す記号]

  • 例 1: H26.03.21 (基本形式: 26.03.21)

注意点として、年月日の区切りに ISO 8601 で定められているハイフン '-' ではなくピリオド '.' を用いること、年は必ず 2 桁になること、基本形式では現在の元号とみなすことが挙げられます。

現在、元号の符号として定められているものは以下の通りです。

table 1 - 元号と西暦の対応表
元号記号当該元号の日付対応する西暦日付
明治 M M01.01.01 (*1) (*4) 1868-01-25
M01.09.08 (*1) 1868-10-23
M05.12.02 (*2) 1872-12-31
M06.01.01 (*2) 1873-01-01
M45.07.29 1912-07-29
大正 T T01.07.30 1912-07-30
T15.12.24 1926-12-24
昭和 S S01.12.25 1926-12-25
S64.01.07 (*3) 1989-01-07
平成 H H01.01.08 1989-01-08

(*1) 元号が (それまでの慶応から) 明治に改元されたのは、実際には天保暦 (太陰太陽暦) の 9 月 8 日ですが、改元は同年の 1 月 1 日まで遡って適用されました。

(*2) 明治 6 年 1 月 1 日をもって、それまでの天保暦からグレゴリオ暦 (西暦) に移行しました。その調整のため、明治 5 年 12 月 2 日の翌日が明治 6 年 1 月 1 日となっています。

(*3) 昭和天皇が昭和 64 年 1 月 7 日に崩御し、翌日に改元された後も、システムが「平成」に対応するまでの間は、移行措置として昭和換算の日付を用いることが許されています (例えば、1990-12-19 を暫定的に S65.12.19 として処理することも可能です)。ただし日付が 2 桁固定のため、システムを改修しないまま 2025 年まで使い続けると日付がオーバーフローする、いわゆる「昭和 100 年問題」が発生します。

(*4) 明治より前の元号 (慶応など) は、記号が定められていません。明治以前はグレゴリオ暦と互換性のない天保暦 (太陰太陽暦) が使用されていたため、JIS X 0301 では取り扱わない (西暦を使用する) ことになっています。

8. まとめ

今回は ISO 8601 を解説しました。Data and Time API は ISO 8601 に基づいて設計されており、API の細かな振る舞いについて「ISO 8601 でそのように規定されているから」としか説明できない場合が多々あります。ISO 8601 という前提知識があるだけで Date and Time API の理解は深まります。

参考文献

JIS X 0301 - http://www.jisc.go.jp/app/pager?%23jps.JPSH0090D:JPSO0023:/JPS/JPSO0090.jsp=&AKKNB_vJISJISNO=X0301

Puzzle : DateTimeFormatter

この記事は Java Puzzlers Advent Calendar 2016 の 6 日目です。

問題

import java.time.*;
import java.time.format.*;

public class DateTimeFormatterSample {
  public static void main(String... args) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/M/d")
                                          .withResolverStyle(ResolverStyle.STRICT);
    LocalDate date = LocalDate.parse("2016/12/06", formatter);
    System.out.println(date.format(formatter));
  }
}

このプログラムは標準出力に何を表示するでしょうか?

(1) 2016-12-06

(2) 2016/12/06

(3) 2016/12/6

(4) DateTimeParseException がスローされる

解答

(4) DateTimeParseException がスローされる

どこがいけないのか?

ResolverStyle が STRICT のときには、パターン文字列 "yyyy/M/d" を使用してパースすることはできません。

どうすれば良いのか?

年 (Year) を示すパターン文字 y は、紀元 (Era) を示すパターン文字 G と組み合わせて使用しなければなりません。例えば、JIS X 0301 で規定された和暦表現を表すパターン文字列は "GGGGGyy.MM.dd" となります (G が元号、y が和暦にそれぞれ対応)。また、西暦 (ISO 8601) の場合には、パターン文字 G は "西暦" などの文字列に対応します。

紀元 (Era) と結びつかない年 (Year) を示すにはパターン文字 u を使用します。このケースでは、パターン文字列を "uuuu/M/d" とすると最も思い通りの結果が得られます。

パースの厳密さを決める ResolverStyle には STRICT (厳密) の他に、SMART (スマート) と LENIENT (非厳密) があります。DateTimeFormatter の既定値は SMART です。LENIENT の場合はパターン文字 y だけが存在しパターン文字 G がないパターン文字列であっても、パターン文字 G のチェックを無視するため、"yyyyy/M/d" と "uuuu/M/d" のどちらでも同じ結果が得られます。SMART はフィールドによって LENIENT または STRICT を自動的に決めます。年 (Year) フィールドについては LENIENT と同じ動きをします。

なお、ResolverStyle が関係してくるのはパース時のみです。フォーマット時には無視されます。従って、フォーマット時のパターン文字列は、このケースでは "yyyyy/M/d" と "uuuu/M/d" のどちらでも良い、ということになります。

教訓

DateTimeFormatter のパターン文字 y と u はきちんと使い分けよう。

JShell で学ぶ Date and Time API

今回は、JDK 9 の JShell を使用して Date and Time API (ThreeTen) を学習する方法についてご紹介します。

1. JShell とは?

JShell とは、Java の構文をインタラクティブに確認できるユーティリティです。いわゆる REPL (Read-Eval-Print-Loop) ツールと呼ばれるもので、OpenJDK の Project Kulla の成果です。既に JShell を同梱する JDK 9 の Early Access 版が下記 URL で公開されているため、手軽に試すことができます。

https://jdk9.java.net/download/

ここでは JShell の細かな使い方までは触れません。Project Kulla のコミッタである吉田真也さん (@bitter_fox) が JShell の紹介記事を公開しているので、そちらを参照してください。

http://d.hatena.ne.jp/bitter_fox/20160522/1463925739

2. JShell で Date and Time API を試す

2.1. JShell の起動と終了

JShell を起動するには、コマンドラインから jshell と入力するだけです。

C:\Users\Kenji>jshell
|  JShellへようこそ -- バージョン9-ea
|  概要については、次を入力してください: /help intro

jshell>

JShell 内では Java のコードを一通り試すことができます (JShell でやるべきか否かは別問題として)。JShell は IDE のようなコード補完機能も持っています。

JShell を終了するには /exit コマンドを使用します。

jshell> /exit
|  終了します

C:\Users\Kenji>

2.2. Date and Time API のインポート

Date and Time API は 5 つのパッケージから成り立っています。とりあえず、全部インポートしてしまいましょう。

jshell> import java.time.*

jshell> import java.time.format.*

jshell> import java.time.chrono.*

jshell> import java.time.temporal.*

jshell> import java.time.zone.*

2.3. 例: 今日の日付

今日の日付 (2016 年 10 月 8 日と仮定します) を取得します。今日の日付は LocalDate.now() メソッドで取得できます。

JShell 上は toString() メソッドの戻り値が表示されます。Date and Time API は一部の例外を除き ISO 8601 に準拠した意味のある文字列を返す設計になっていますので、結果の日付や時刻をその場で確認できます。

jshell> LocalDate.now()
$6 ==> 2016-10-08

3. Date and Time API のサンプル

Date and Time API の詳細については以下のスライド (2015 年 7 月 : 関ジャバ「Java 8徹底再入門」発表資料) を参照してください。

3.1. 様々な日付と時刻

LocalDate と LocalDateTime で暦日付 (calendar date) と年間通算日 (ordinal date) を取得します。暦日付は LocalDate.of() または LocalDateTime.of() メソッド、年間通算日は LocalDate.ofYearDay() メソッドでそれぞれ取得できます。

jshell> LocalDate.of(2014, 3, 21)
$7 ==> 2014-03-21

jshell> LocalDate.ofYearDay(2014, 80)
$8 ==> 2014-03-21

jshell> LocalDateTime.of(2014, 3, 21, 15, 30)
$9 ==> 2014-03-21T15:30

3.2. 時差・タイムゾーンを持つ日時

時差を持つ日付は OffsetDateTime.of() メソッド、タイムゾーンを持つ日付は ZonedDateTime.of() メソッドでそれぞれ取得できます。書式や保持する情報が類似する LocalDateTime.of() メソッドと見比べてみてください。また、ZoneOffset.ofHours() や ZoneId.of() メソッドの引数を変えて、様々な時差やタイムゾーンを試してみてください。

jshell> LocalDateTime.of(2014, 3, 21, 15, 30)
$9 ==> 2014-03-21T15:30

jshell> OffsetDateTime.of(2014, 3, 21, 15, 30, 45, 0, ZoneOffset.ofHours(9))
$10 ==> 2014-03-21T15:30:45+09:00

jshell> ZonedDateTime.of(2014, 3, 21, 15, 30, 45, 0, ZoneId.of("Asia/Tokyo"))
$11 ==> 2014-03-21T15:30:45+09:00[Asia/Tokyo]

3.3. LocalDate から LocalDateTime および OffsetDateTime への変換

LocalDate は時刻情報を付加して LocalDateTime、さらに時差情報を付加して OffsetDateTime へと変換することができます。LocalDate には変換メソッドが数多く用意されていますので、JShell で試してみてください。LocalDate の小回りの良さが実感できると思います。

jshell> LocalDate localDate = LocalDate.of(2014, 3, 21)
localDate ==> 2014-03-21

jshell> localDate.atStartOfDay()
$13 ==> 2014-03-21T00:00

jshell> localDate.atTime(15, 30, 45)
$14 ==> 2014-03-21T15:30:45

jshell> localDate.atTime(15, 30, 45).atOffset(ZoneOffset.ofHours(9))
$15 ==> 2014-03-21T15:30:45+09:00

3.4. OffsetDateTime から LocalDate および LocalTime への変換

OffsetDateTime から日付部分を toLocalDate() メソッド、時刻部分を toLocalTime() メソッドでそれぞれ取得できます。ZonedDateTime でも同じことが可能なため、試してみてください。

jshell> OffsetDateTime offsetDateTime = OffsetDateTime.of(2014, 3, 21, 15, 30, 45, 0, ZoneOffset.ofHours(9))
offsetDateTime ==> 2014-03-21T15:30:45+09:00

jshell> offsetDateTime.toLocalDate()
$17 ==> 2014-03-21

jshell> offsetDateTime.toLocalTime()
$18 ==> 15:30:45

3.5. 閏年判定

閏年判定を行うメソッドは、LocalDate.isLeapYear() と Year.isLeap() の 2 つです。ここでは LocalDate.isLeapYear() を用いて様々な日付について閏年判定をしてみましょう。

jshell> LocalDate.of(1990, 12, 19).isLeapYear()
$19 ==> false

jshell> LocalDate.of(1992, 10, 8).isLeapYear()
$20 ==> true

jshell> LocalDate.of(2000, 1, 1).isLeapYear()
$21 ==> true

3.6. 日付の加算と減算

Date and Time API の強力な機能のひとつである日付の加算・減算についても、JShell で簡単に試すことができます。フィールドの取得 (get-) や変更 (with-) も、JShell を使えば手間をかけずに結果を確認できます。

jshell> LocalDate today = LocalDate.of(2014, 3, 21)
today ==> 2014-03-21

jshell> today.minusWeeks(1)
$23 ==> 2014-03-14

jshell> today.plusDays(2)
$24 ==> 2014-03-23

jshell> today.minusWeeks(1).until(today.plusDays(2))
$25 ==> P9D

3.7. その他

他にも、JShell を使うことで以下の機能を試すことができます。これらをサンプルプログラムで確認するのは相応の手間がかかるため、JShell は非常にありがたい存在です。

  • 比較 (isAfter/isEqual/isBefore)
  • フォーマット (format/parse および DateTimeFormatter)
  • ローカル暦 (JapaneseDate など)

4. まとめ

Date and Time API を学習するには、以下の手順を踏むのが (遠回りに見えても) 一番の近道でしょう。

Step 1. ISO 8601 (または JIS X 0301) を学ぶ。

Step 2. LocalDate クラスの使い方をマスターする。

Step 3. なぜ Local/Offset/Zoned の3種類のクラスが存在するのかを知る。

Step 4. 自分達の仕事で重点的に使う範囲を絞り込む。

Step 5. あとはトライ・アンド・エラー!

Java SE 8 では、最も重要な Step 5 の実践に手間がかかってしまうという難点がありました。JShell を使えば、API の挙動を確認することだけに専念できるため、学習効率は大幅に向上します。

Date and Time API への招待

Date and Time API については、ブログで書いては消し、を続けてきましたが、 昨年 7 月に大阪で開催された Java 8徹底再入門発表内容をもとに、 当日割愛した TemporalAdjuster と暦フレームワークのさわりの部分を追記したスライドを作成しましたので、 当該スライドとその補足をもって総論にしたいと思います。

なお、Date and Time API の基礎理論である ISO 8601 については、事実上の邦訳である JIS X 0301 を用いて 解説した記事を掲載していますので、必要に応じてご覧ください。

Docs.com https://doc.co/Ze2W25 からも資料をダウンロードできます。

1. 「時」とは?

時の基本単位は「秒」と定められています。 「秒」は7つある SI 基本単位(他の単位の組み合わせでは表せない単位)の1つとなっています。 「秒」を基本として、時刻は次のように定義されています。

  • 1 分 = 60 秒
  • 1 時間 = 60 分 = 3,600 秒
  • 1 日 = 24 時間 = 86,400 秒

もっとも、歴史的には逆で、1日の半分を占める日の出~日没(または日没~日の出)を 12 等分したものを 1 時間、 1 時間の 60 分の 1 が 1 分、1 分の 60 分の 1 が 1 秒とされ、数千年に渡って使用されてきました。

100 等分でなく 60 等分を用いたのは、現在のような時のシステムを作り上げた古代メソポタミア文明のバビロニア王朝で、 60 進法が日常的に使われていたためと言われています。

また、1 日を 10 でなく 12 等分したのは、インドで 0 の概念が発見される前に時のシステムが成立したためです。 0 が発見されるまで 10 進法はマイナーで、2、3、4、6 のいずれでも割り切れる 12 が位取りとして世界各所で好まれていました。

現在のように「秒」を時の基準とするようになったのは 20 世紀半ば以降です。 観測技術が進み 1 日の長さが厳密には一定でないことがわかり、代わりに秒を超高精度に刻むことが可能な原子時計を基準とすることにしました。原子時計に関する詳細は割愛しますが、天体観測の結果より桁違いに精度が高く体感できる時刻とかけ離れてしまうことから、原子時計の時刻に「うるう秒」を加えて天体観測の結果に近づけた UTC(協定世界時)が基準として用いられます。

Date and Time API では、うるう秒は考慮しません(仕様検討時には考慮するモードもありました)。

さて、私たちが使用している時刻は、例えば日本では JST(日本標準時)であり、UTC と異なることが少なくありません。太陽が南中した時刻をその土地の 12 時とする時刻を地方時と言い、JST は地方時の1つです。JST は UTC よりも 9 時間進んでいるため、UTC で 0 時ちょうどの時、JST では 9 時となります。さらにアメリカ西海岸の地方時である PST は UTC より 8 時間遅れているため、UTC で 0 時ちょうどの時、PST では前の日の 16 時となります。これが時差と呼ばれるものです。

ISO 8601 では UTC からのずれで時差を表現しますが、コンピュータ上では同じ時差を持つ地域であるタイムゾーンを Time Zone Database(tz database)としてデータベース化して利用しています。tz database は時差に加え夏時間の情報、さらにはこれらの履歴をも保持しています。tz database は JDK のアップデートのタイミングで最新版に更新されます。

2. ISO 8601 について

ISO 8601 は日付と時刻の表記に関する国際標準で、コンピュータ上で確実に取り扱うことを第一義として定められています。 ISO 8601 は一般に用いられているグレゴリオ暦を用います(一部のキリスト教会で用いるユリウス暦ではない)。

JIS X 0301 は ISO 8601 を翻訳して日本独自の和暦表記の規則を追加した日本標準規格で、その他の国々も ISO 8601 ベースの標準規格を定めています。 また、ISO 8601 のサブセットとしてインターネット標準として用いられている RFC 3339 があります。

ここで注意したいことは、ISO 8601 はコンピュータ上で普及している Unix 時刻と互換性がないということです。 このことは、ISO 8601 に準拠した Date and Time API は、旧来の java.util.Data / Calendar との互換性がないことを意味します。 意外と見落とされがちな事実なので注意してください。

以降、ISO 8601 の表記法についてはスライドを参照してください。

3. Date and Time API の基本

スライドを参照してください。

4. 高度なトピック

この章の内容は高度なトピックのため、最初は飛ばしてしまっても構いません。

4.1. Instant と Clock

スライドを参照してください。

4.2. ZoneId と ZoneOffset

スライドを参照してください。

4.3. Temporal オブジェクトとは?

スライドを参照してください。

4.4. 暦のサポート

スライドを参照してください。

5. まとめ

Date and Time API を一言で表すなら、「日付と時刻の標準規格 ISO 8601 をモデリングした API」です。 クラス数は少なくありませんが、一つひとつの基本的な使い方は難しくありません。

日付・時刻に関する演算が充実しており、それは旧来の java.util.Data や java.util.Calendar と比べると一目瞭然です。 特に TemporalAdjuster は、例えば「次の土曜日」のように抽象的な日付を算出できる強力な機能です。

また、多数の拡張ポイントを備えていることも特徴です。 暦サポートクラスを収めた java.time.chrono パッケージを見て、その柔軟性を確認してください。

Date and Time API は初学者にとって敷居の高い API の一つとみなされています。 しかし、手順を踏んで学べば決して難しいものではありません。 日付や時刻といった日常にあるものを表現している分だけイメージはしやすいはずです。

Step 1. ISO 8601(または JIS X 0301)を学ぶ。

Step 2. LocalDate クラスの使い方をマスターする。

Step 3. なぜ Local/Offset/Zoned の3種類のクラスが存在するのかを知る。

Step 4. 自分達の仕事で重点的に使う範囲を絞り込む。

Step 5. あとはトライ・アンド・エラー!

幸い、JDK 9 からは JShell という REPL が導入され、上記 Step 5 を中心に試しやすくなります。 使い込むほど便利な API なので、ぜひ習得してみてください。

7 月 11 日 (土) に大阪で開催された「Java 8徹底再入門」(主催: 関西 Java エンジニアの会) にて、Date and Time API のお話をしました。同 API の包括的な解説は JJUG で何回かやっているのですが、大阪でお話しするのは今回が初めてになります (大阪にまとまった時間滞在すること自体が初めてだったりする)。

今回のプレゼンでは、以下の 2 点を目標としました。うまく達成できていたでしょうか?

  • 初めての方 (もしくは一度挫折してしまった方) には、これを機会に Date and Time API を使ってみようと思ってもらえること。
  • 既に使っている方には、ZonedDateTime 一択という使い方について本当に妥当かどうか考え直してもらえること。

発表資料を以下に示します。JJUG CCC 2013 Fall の「JSR 310 "Date and Time API" への招待 II」のストーリーをベースにしながら、2014 年 3 月の Java 8 ローンチ・イベント、#渋谷java での発表内容を盛り込んだものに仕上げました。

これは何度も言っていますが、Date and Time API は ISO 8601 (コンピュータ間でデータのやりとりをする際の、日付と時刻の書式に関する国際規格) をモデリングして作られたもので、ISO 8601 について知らないと API が「なぜ」そのように設計されたのかわからず、適切でない使い方をしてしまうケースが少なくありません。その結果、「Date and Time API は使い勝手が悪い」と早とちりしてしまいがちです。とは言っても、実際に知っておかなければならない ISO 8601 の予備知識は今回の発表スライドに挙げた程度で、それ以上細かい規定については知らなくても大丈夫でしょう。

Date and Time API は public なクラスだけでも 69 個ありますが、そのうち常時使うのは多くても 20 個程度です。これについては、API を扱うプログラマ、あるいはアプリケーションの要求によって異なってくるため、自分自身の感覚でプライオリティを付けておくことが大切です。最初のとっかかりとしては LocalDate を集中的に勉強するとよいでしょう。LocalDate は標準的な操作が一通り揃っており、かつそれほど複雑ではありません。

ある程度慣れてきたら、多くの例を見ておくとよいでしょう。Angela Caicedo が Java Day Tokyo 2015 のセッションで使用した資料は、サンプル集としておすすめです。


今回は筆者のセッションと、@bitter_fox さんの Lambda/Stream API ハンズオンの二本立てでした。

本当にわかっている人がやる説明って、こういうものなのですよ。参加された方は、本当にツイてます!


今回は、何気ない連投ツイートがきっかけで @backpaper0 さんが企画してくださり、@bufferings さん、@s_kozake さん、@irof さん、@yukieen さん、@haljik さんにはスタッフとして勉強会の進行や懇親会の企画、さらには宿探しや当日の道案内まで買って出てくださいました。さらには持ち時間の 50 分を大幅に超過しても (自分で言うのも何ですが、今回レベルの大幅超過は極めて稀です) 会場の皆さんは最後までお付き合いくださいました。皆さんのおかげで、がんばりました。がんばれました。ありがとうございました。

TemporalAdjuster の使い方

2015-03-07 に開催された 第十回 #渋谷java で「Date and Time API の TemporalAdjuster 活用法」と題して、Date and Time API の低水準 API である TemporalAdjuter の使い方をお話ししました。

TemporalAdjuster を含む低水準 API は、昨年 3 月の Java 8 ローンチ・イベント(日本 Java ユーザーグループ主催)では発表内容から外しました。低水準 API まで踏み込むよりは前提知識として ISO 8601 の解説をした方が良いと判断したからです。

本当は ISO 8601 や日付と時刻の成り立ちの話より、Date and Time API のコード例をできるだけ多く例示した方が良かったのかもしれません。しかし、日本オラクルのセミナールームは最大で定員 200 名の大きな部屋(当日は無理矢理詰め込んで 300 名近い方々がいらしたようです)なので、スクリーンにサンプルコードを映しても前方 4 分の 1 くらいを除いて、文字が小さすぎて見えなかったと思います。

その後、東京以外の場所でも Java 8 のローンチ・イベントが行われ、そちらでは他の Java SE 8 新機能と合わせて寺田さんが Date and Time API の話をしていて、その中で TemporalAdjuster を使ったサンプルを出していたようです。ただ、内容的には「こんなこともできるようになったよ」程度の、ほんのさわりの部分だけで、TemporalAdjuster の名前自体も明示していなかったと思います。

また、Date and Time API のチュートリアルで TemporalAdjuster がどういった存在であるか示されていますが、具体的なことについてはほとんど触れられていません。軽く探してみた範囲では、少なくとも国内では TemporalAdjuster の説明を実装まで含めて行っている例はありませんでした。そういった意味では、今回の発表は小さいけれどもエポックメイキングなものであったと考えています。

では、発表内容についておさらいと補足をします。

1. Temporal と TemporalAdjuster

Date and Time API では、日付や時刻などをまとめて Temporal objects と呼んでおり、Temporal objects はすべて Temporal インタフェースを実装することになっています(実装していないクラスは日付や時刻の一部分を表すだけのものです)。この Temporal を変更するために用意されているのが TemporalAdjuster です。具体的には Temporal.with メソッドに TemporalAdjuster を渡すことで Temporal の内容が変更されます。実際には既知の Temporal 実装クラスはすべて immutable のため、内容を変更したコピーが返ることになります。TemporalAdjuster は Temporal から独立しているため(Strategy パターン)、一度作成すれば互換性のある Temporal すべて共通で再利用が可能です。

2. TemporalAdjusters ユーティリティ

TemporalAdjuster は再利用可能なため、ライブラリとして蓄積しておくと有益です。幸い、Java 標準で利用頻度の高い TemporalAdjuster が用意されており、TemporalAdjusters ユーティリティ・クラスの static メソッドで呼び出すことができるようになっています。

3. TemoralAdjuster の独自実装

多くのアプリケーションでは、TemporalAdjusters ユーティリティが用意している Adjuster があれば事足りますが、より複雑な要件には対応できない場合があります。例えば、営業日、春分/秋分、振替休日などの算出はそれなりの規模の判定が入りますので、標準の Adjuster では対応できません。ただし、例えば営業日を計算する Adjuster を一度実装してしまえば、以後営業日計算で毎回判定プログラムを書く必要がなくなります。

春分の日と秋分の日は、前年 2 月 1 日の官報による告知で正式決定されます(逆説的に表現すると、2015 年 3 月時点では 2016 年までの春分の日/秋分の日しか決定されていない)。ただし、予測のための算出式を用いればある程度の将来まで求めることができます。

TemporalAdjuster の正体は引数・戻り値がともに Temporal の関数インタフェースです(ということはラムダで書くことが想定されています)。今回の発表では、条件を付けた上で翌営業日を求める Adjuster を例として示しました。