kagamihogeの日記

kagamihogeの日記です。

The Java TutorialsのDate-Timeのところをテキトーに訳した

Java 8のDate-Time API良く知らんのだよな~ということでThe Java TutorialsDate-Time のセクションを読んだ。 例によって機械翻訳よりごましお程度にマシな精度なのでそこのところご容赦願いたい。

Trail: Date Time

Java SE 8リリースで導入された、Date-Timeパッケージ、java.time、日付(date)と時間(time)に関する包括的なモデルを提供するもので、JSR 310: Date and Time APIの下で開発されました。java.time国際標準化機構(International Organization for Standardization:ISO)カレンダーシステムに基づきますが、一般的に使用されるグローバルなカレンダーもサポートされています。

このトレールは、日付と時間を表現するためと、日付と時間の値を操作するための、ISOベースのクラスの使い方の基本を扱います。

Lesson: Date-Time Overview

時間とは、単純なテーマのように見えます。安価な時計でもかなり正確な日付と時間を扱えます。しかしながら、詳細な調査をすると、微妙な複雑性とあなたの時間理解に影響を及ぼす多くの要因があることに気付きます。たとえば、1月31日に一か月を足した結果は、うるう年かそうでないかで異なるものとなります。タイムゾーン(Time zones)もまた複雑さを増加させます。たとえば、ある国は突然にサマータイムに入ったり出たりするかもしれませんし、年に一回以上かもしれないし、または、ある年は完全にサマータイムをスキップするかもしれません。

Date-Time APIは、デフォルトカレンダーとして、ISO-8601で定義されたカレンダーシステムを使用します。このカレンダーはグレゴリオ暦(Gregorian calendar)に基づくもので、日付と時間を表現するためのグローバルなデファクトスタンダードとして使われています。Date-Time APIのコアクラスは、LocalDateTime, ZonedDateTime, OffsetDateTimeのような名前を持っています。これらはすべてISOカレンダーシステムを使用します。もし、Hijrah*1やThai Buddhist*2のような別のカレンダーシステムを使用したい場合、java.time.chronoパッケージで事前に定義されたカレンダーシステムの内の一つを使うことができます。もしくは、自身用のものを作成することもできます。

Date-Time APIは、Unicode Common Locale Data Repository (CLDR)を使用します。このリポジトリは世界の言語をサポートし、利用可能なロケールデータの世界最大のコレクションを持っています。このリポジトリの情報は数百の言語にローカライズされています。また、Date-Time APITime-Zone Database (TZDB)を使用します。このデータベースは、その概念が導入されて以来の主要なタイムゾーンの歴史と共に、1970年以来のすべてのタイムゾーンが変更した情報についての情報を提供しています。

Date-Time Design Principles

Date-Time APIは、いくつかの設計原則に基づいて開発されました。

Clear

APIメソッドは明確に定義され、その振る舞いはクリアで予測可能なものです。たとえば、Date-Timeのメソッドnullパラメータ値で呼び出すことは、おおむねNullPointerExceptionとなります。

Fluent

Date-Time APIは、流れるようなインタフェース(fluent interface)を提供しており、コードを読みやすくします。多くのメソッドnull値のパラメータを許可せず、null値を返さないので、メソッドはが互いにチェーンさせることができ、結果としてコードはすぐに理解できるものになります。たとえば、

LocalDate today = LocalDate.now();
LocalDate payday = today.with(TemporalAdjuster.lastDayOfMonth()).minusDays(2);

Immutable

Date-Time API のクラスの大半はイミュータブル(immutable)なオブジェクトを生成します。この意味は、生成されたオブジェクトは、変更不能ということです。イミュータブルなオブジェクトの値を変更するには、新しいオブジェクトをオリジナルの修正コピーとして生成する必要があります。このことはまた、Date-Time APIはスレッドセーフで定義されていることも意味しています。日付や時間のオブジェクトを生成するためには、コンストラクタよりはむしろ、of, from, withのプレフィクスを持つメソッドが多数となり、また、setメソッドは存在しません。たとえば、

LocalDate dateOfBirth = LocalDate.of(2012, Month.MAY, 14);
LocalDate firstBirthday = dateOfBirth.plusYears(1);

Extensible

Date-Time APIは可能な限り、拡張可能です。たとえば、時間調整(time adjusters)とクエリ(queries)を定義したり、自前のカレンダーシステムを構築することができます。

The Date-Time Packages

Date-Time APIは、java.timeと4つのサブパッケージから構成されています。

  • java.time
    日付と時間を表現するためのコアAPI。日付、時間、日付と時間の組み合わせ、タイムゾーン、瞬間(instants)、期間、時計、のためのクラスが含まれています。これらのクラスはISO-8601で定義されるカレンダーシステムに基づいており、イミュータブルでスレッドセーフです。
  • java.time.chrono
    デフォルトのISO-8601以外のカレンダーシステムを表現するためのAPI。自前のカレンダーシステムを定義可能です。このチュートリアルではこのパッケージの詳細については触れません。
  • java.time.format
    日付と時間のフォーマットとパースのためのクラス。
  • java.time.temporal
    主にフレームワークとライブラリ作成者のための、拡張されるAPIで、日付と時間クラス間の相互作用や、クエリと時刻合わせなどが可能です。このパッケージには、フィールド(Fields)(TemporalFieldChronoField)とユニット(units)(TemporalUnitChronoUnit) が定義されています。
  • java.time.zone タイムゾーンをサポートするクラスで、タイムゾーンのオフセットやタイムゾーンルールがあります。もしタイムゾーンを使用する場合、多くの開発者はZonedDateTimeZoneIdZoneOffsetのみ使う必要があります。

Method Naming Conventions

Date-Time APIは、豊富なクラスとメソッドを提供しています。メソッド名はクラス間で可能な限り一貫性があるように構成されています。たとえば、多くのクラスは、そのクラスに関連する現在の瞬間の日付もしくは時間を捕捉する、nowメソッドを提供します。fromメソッドは、あるクラスから別のクラスへの変換を行います。

メソッド名のプレフィクスには統一性を持たせています。Date-Time APIのクラスの多くはイミュータブルなので、APIsetメソッドを持ちません。(生成後、イミュータブルオブジェクトの値は変更不可能です。イミュータブルのsetと同等なものはwithです。)以下の表は良く使われるプレフィクスの一覧です。

プレフィクス メソッドタイプ 使い方
of static factory 主にファクトリが入力パラメータを検証し、変換をせずに、インスタンスを生成します。
from static factory 入力パラメータをターゲットクラスのインスタンスへ変換する。このとき、入力値の情報を切り捨てる可能性があります。
parse static factory ターゲットクラスのインスタンス生成のために入力文字列をパースします。
format instance 指定されたformatterで、文字列生成のための一時オブジェクトで値をフォーマットします。
get instance ターゲットオブジェクトの状態の一部を返します。
is instance ターゲットオブジェクトの状態を問い合わせます。
with instance ある要素が変更されたターゲットオブジェクトのコピーを返します。JavaBeanにおけるsetメソッドと同等なものです。
plus instance 時間を加算したターゲットオブジェクトのコピーを返します。
minus instance 時間を減算したターゲットオブジェクトのコピーを返します。
to instance 別の型へオブジェクトを変換します。
at instance 別のオブジェクトと結合します。

Lesson: Standard Calendar

Date-Time APIのコアパッケージはjava.timeです。java.timeで定義されているクラスはISOのカレンダーシステムに基づいており、これは世界的に標準的な日付と時間の表現方法です。ISOカレンダーはproleptic Gregorian rules*3に従います。グレゴリオ暦は1582年に導入されて、proleptic Gregorian calendarでは、日付は、一貫性のある時刻生成、統一された年表、日付計算を単純化するため、過去方向*4へ拡張されています。

このレッスンは以下のトピックを扱います。

Overview

このセクションでは、人的時間(human time)と、java.timeパッケージの主要なtemporal-basedクラスを表にまとめたマシンタイム(machine time)のコンセプトを比較します。

DayOfWeek and Month Enums

このセクションでは、曜日(DayOfWeek)定義のenumと月(Month)定義のenumについて説明します。

Date Classes

このセクションでは、時間とタイムゾーンを含まない、日付のみを扱うtemporal-basedクラスを示します。4つのクラスがあり、それぞれLocalDate, YearMonth,MonthDay, Yearです。

Date and Time Classes

このセクションでは、LocalTimeLocalDateTimeクラスについて説明します。これらはそれぞれ、時間、日付と時間、を扱うもので、ただし、タイムゾーンは含みません。

Time Zone and Offset Classes

このセクションでは、タイムゾーン(もしくはタイムゾーンオフセット)を格納するtemporal-basedクラス、ZonedDateTime, OffsetDateTime, OffsetTime、について説明します。サポートクラスであるZoneId, ZoneRules, ZoneOffsetについても説明します。

Instant Class

このセクションでは、時間の流れにおけるある瞬間を表現するための、Instantクラスについて説明します。、

Parsing and Formatting

このセクションでは、事前定義されたフォーマットと日付および時間のパースの使用方法の概要を示します。

The Temporal Package

このセクションでは、temporalクラス、フィールド(TemporalFieldChronoField)とユニット(TemporalUnitChronoUnit)、をサポートするjava.time.temporalパッケージの概要について説明します。また、"the first Tuesday after April 11(4/11後で最初の火曜日)"のような、調整時刻を検索するためのtemporal adjusterの使用方法と、temporal queryの実行法について、説明します。

Period and Duration

このセクションでは、時間の合計の計算方法について、PeriodDurationクラス、ChronoUnit.betweenメソッドを使用して、説明します。

Clock

このセクションでは、Clockクラスの概要について説明します。このクラスを使用することで、システム時計の代わりとなる時計を提供できます。

Non-ISO Date Conversion

このセクションでは、ISOカレンダーシステムの日付から、JapaneseDateThaiBuddhistDateのような、非ISOの日付へと変換する方法について説明します。

Legacy Date-Time Code

このセクションでは、旧来のjava.util.Datejava.util.CalendarコードからDate-Time APIへ変換する方法についていくつかのTipsを示します。

Summary

このセクションでは、Standard Calendarレッスンのサマリを示します。

Overview

時間を表現する一般的な方法は二つあります。一つは人間にとっての時間表現で、年、月、日、時、分、秒のような、human timeと呼ばれるものです。もう一つの方法は、epochと呼ばれるもので、ある起点からの時間の流れをナノ秒単位で計測する、machine timeです。Date-Timパッケージは日付と時間を表現するクラスを豊富に提供しています。Date-Time APIのいくつかのクラスはmachine timeを表現するために使用され、その他はhuman timeを表現するために使用できます。

まず決定が必要なのは日付と時間に関する要求で、それから要求を満たすクラスを選択します。temporal-basedクラスを選択するとき、まず最初にhuman timeかmachine timeのどちかが必要とされているか、を決定します。それから、表現する必要のある時間の要求について確認します。タイムゾーンは必要なのか? 日付時間なのか? 日付のみか? もし日付が必要な場合、月・日年、もしくはそのサブセットか?

用語(Terminology):Date-Time APIのクラスは日付もしくは時間の値を扱うために、このチュートリアルにおいて、Instant,LocalDateTime, ZonedDateTimeのような、temporal-basedクラス(もしくは型)と呼ばれるものを使用します。TemporalAdjusterインタフェースもしくはDayOfWeekenumのようなサポート型は、この定義には含まれません。

たとえば、誕生日を表現するにはLocalDateオブジェクトを使用しますが、その理由は、彼らが同じ場所で生まれようと国際日付変更線の反対側の地球上で生まれようと、ほとんどの人は誕生日が同じ人がいるためです*5。もし占星術的な時間を記録する場合、出生の日付と時間を表現するためにLocalDateTimeオブジェクトか、タイムゾーンを含むZonedDateTimeを使用することになるでしょう。もしタイムスタンプを生成する場合、`時間軸上のある瞬間と別の瞬間とを比較できる、Instantを使用するでしょう。

以下の表はjava.timeパッケージのtemporal-basedクラスの、日付と時間の情報を格納するか、時間を計測することが出来るか、について要約したものです。列のチェックマークは、そのクラスが使用する個々のデータタイプを示しており、toString Output列はtoStringメソッドを使用してインスタンスを出力したときの様子を示しています。Where Discussed列はチュートリアルの関連ページへのリンクです。

※訳注:下記表のうち、チェックマークは○、Where Discussedのリンクはめんどいんで省略。

ClassかEnum 秒* ZoneOffset ZoneID toString Output Where Discussed
Instant 2013-08-20T15:16:26.355Z
LocalDate 2013-08-20
LocalDateTime 2013-08-20T08:16:26.937
ZonedDateTime 2013-08-21T00:16:26.941+09:00[Asia/Tokyo]
LocalTime 08:16:26.943
MonthDay --08-20
Year 2013
YearMonth 2013-08
Month AUGUST
OffsetDateTime 2013-08-20T08:16:26.954-07:00
OffsetTime 08:16:26.957-07:00
Duration ** ** ** PT20H (20 hours)
Period *** *** P10D (10 days)

  * 秒はナノ秒精度で取得される。
  ** このクラスは印がつけられた情報は保存しませんが、ユニットの時間を提供するためのメソッドは持っています。
  *** ZonedDateTimePeriodが加えられたとき、サマータイムもしくは他のローカル時間の差が生まれます。

DayOfWeek and Month Enums

Date-Time APIは曜日と月を指定するためのenumを提供しています。

DayOfWeek

DayOfWeek enumは、週の曜日を意味する、MONDAYからSUNDAYの7個の定数を持っています。DayOfWeekの整数値の範囲は1(Monday)から7(Sunday)です。定義済みの定数値(DayOfWeek.FRIDAY)は可読性が高いです。

また、このenumはtemporal-basedクラスで提供されるメソッドと似ているメソッドをいくつか持っています。たとえば、以下のコードは"Monday"に3日を足して結果を出力しています。出力は"THURSDAY"になります。

System.out.printf("%s%n", DayOfWeek.MONDAY.plus(3));

getDisplayName(TextStyle, Locale)メソッドを使用することで、ユーザーのロケールで曜日を指定する文字列を検索できます。TextStyle enumで表示したい文字列の種類を指定できます。FULL, NARROW(一般的には一文字), SHORT(省略形)。STANDALONE TextStyle定数値はいくつかの言語で使用されるもので、単独で用いる場合より日付の一部として使用される場合に出力が異なってきます。以下の例は、"Monday"をTextStyleの三つの主要な形式で表示するものです。

DayOfWeek dow = DayOfWeek.MONDAY;
Locale locale = Locale.getDefault();
System.out.println(dow.getDisplayName(TextStyle.FULL, locale));
System.out.println(dow.getDisplayName(TextStyle.NARROW, locale));
System.out.println(dow.getDisplayName(TextStyle.SHORT, locale));

このコードはenロケールでは以下の出力をします。

Monday
M
Mon

Month

Month enumは、JANUARYからDECEMBERの12か月の定数値を持っています。DayOfWeek enumと同様に、Month enumは強く型付がされており、各定数の整数値はISOの1(一月January)から12(十二月:December)の範囲と一致しています。定義済みの定数((Month.SEPTEMBER)はコードの可読性を高めます。

また、Month enumはいくつかのメソッドを持ちます。以下のコードはmaxLengthメソッドを使用して二月の日数の最大値を表示しており、出力は"29"になります。

System.out.printf("%d%n", Month.FEBRUARY.maxLength());

Month enumは、getDisplayName(TextStyle, Locale)メソッドを実装しており、これは指定されたTextStyleを使用して、ユーザーのロケールでの月を表す文字列を取得するためのものです。もしTextStyleが定義されていない場合、定数の整数が文字列として返されます。以下のコードは主な三つのテキストスタイルを使用して、八月(August)を表示するものです。

Month month = Month.AUGUST;
Locale locale = Locale.getDefault();
System.out.println(month.getDisplayName(TextStyle.FULL, locale));
System.out.println(month.getDisplayName(TextStyle.NARROW, locale));
System.out.println(month.getDisplayName(TextStyle.SHORT, locale));

このコードはenロケールでは下記の出力になります。

August
A
Aug

Date Classes

Date-Time APIは、時間もしくはタイムゾーンを除外して、日付情報を扱う4つのクラスを提供します。これらのクラスの使用法は、クラス名(LocalDate, YearMonth, MonthDay, Year)が示唆しています。

LocalDate

LocalDateはISOカレンダーの年・月・日を表現しており、時間を除いた日付を表現することに使用できます。LocalDateは、誕生日や結婚記念日のような、特定のイベントを記録することに使用できます。以下の例は、LocalDateインスタンスを作成するために、ofwithメソッドを使用しています。

LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 20);
LocalDate nextWed = date.with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));

TemporalAdjusterインタフェースの詳細については、Temporal Adjusterを参照してください。

通常のメソッドに加えて、LocalDateクラスは、与えられた日付に関する情報を取得するためのgetterメソッドを提供しています。getDayOfWeekメソッドは、その日付に対応する曜日を返します。たとえば、以下のコード例は"MONDAY"を返します。

DayOfWeek dotw = LocalDate.of(2012, Month.JULY, 9).getDayOfWeek();

以下の例は、指定された日付以降で最初の水曜日(Wednesday)を検索するために、TemporalAdjusterを使用しています。

LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 20);
TemporalAdjuster adj = TemporalAdjusters.next(DayOfWeek.WEDNESDAY);
LocalDate nextWed = date.with(adj);
System.out.printf("For the date of %s, the next Wednesday is %s.%n",
                  date, nextWed);

このコードの実行結果は以下のようになります。

For the date of 2000-11-20, the next Wednesday is 2000-11-22.

また、Period and DurationセクションでもLocalDateクラスの使用例を紹介します。

YearMonth

YearMonthクラスは、特定の年月を表現するものです。以下の例では、YearMonth.lengthOfMonth()メソッドを使用して、ある年のある月における日数を取得する例を示しています。

YearMonth date = YearMonth.now();
System.out.printf("%s: %d%n", date, date.lengthOfMonth());

YearMonth date2 = YearMonth.of(2010, Month.FEBRUARY);
System.out.printf("%s: %d%n", date2, date2.lengthOfMonth());

YearMonth date3 = YearMonth.of(2012, Month.FEBRUARY);
System.out.printf("%s: %d%n", date3, date3.lengthOfMonth());

このコードの出力は以下のようになります。

2013-06: 30
2010-02: 28
2012-02: 29

MonthDay

MonthDayクラスは、新年である1月1日のような、特定の月・日を表現するものです。

以下の例はMonthDay.isValidYearメソッドを使用して、2/29が2010年において正しいかどうかを確認しています。この呼び出しはfalseを返し、2010年はうるう年ではないことを確認しています。

MonthDay date = MonthDay.of(Month.FEBRUARY, 29);
boolean validLeapYear = date.isValidYear(2010);

Year

Yearクラスは年を表現するものです。以下の例は、Year.isLeadメソッドを使用して、与えられた年がうるう年かどうかを確認しています。この呼び出しはtrueを返し、2012年はうるう年であることを確認しています。

boolean validLeapYear = Year.of(2012).isLeap();

Date and Time Classes

LocalTime

LocalTimeクラスは、他のLocalプレフィクスを持つクラスと似ていますが、時間のみを扱います。このクラスが役に立つのはhuman-basedな時間を表現するときで、例えば、映画の上映時間や、地域図書館の開館と閉館時間などです。また、デジタル時計を作成するのにも使用でき、以下の例のようになります。

LocalTime thisSec;

for (;;) {
    thisSec = LocalTime.now();

    // displayの実装は課題として残してあります。
    display(thisSec.getHour(), thisSec.getMinute(), thisSec.getSecond());
}

LocalTimeクラスはタイムゾーンサマータイムの情報を持ちません。

LocalDateTime

このクラスは日付と時間の両方を扱いますが、タイムゾーンは持ちません、

Date-Time APIのコアクラスの一つであるLocalDateTimeクラスは、日付と時間の両方を扱います。ただし、タイムゾーンは持ちません。このクラスは、日付(年・月・日*6)と時間(時・分・秒・ナノ秒)を表現するために使用するもので、実質的には、LocalDateLocalTimeを組み合わせたものです。このクラスはある特定の出来事を表現するために使用でき、たとえば、Louis Vuitton Cup Finals in the America's Cup Challenger Series*7の最初のレースの開始時刻の2013年8月17日 午後1:10、などです。注意点として、午後1:10はローカル時間です。タイムゾーンを含めるためには、ZonedDateTimeもしくはOffsetDateTime使用する必要があります。詳細については、Time Zone and Offset Classesで解説します。

どのtemporal-basedクラスでも提供しているnowメソッドに加えて、LocalDateTimeクラスは様々なofメソッド(ないしofプレフィクスのメソッド)を持っており、これらはLocalDateTimeインスタンスを生成します。fromメソッドは、インスタンスを別のtemporalフォーマットからLocalDateTimeインスタンスへと変換します。また、時・分・日・週・月を加算・減算するためのメソッドもあり、以下にその例を示しています。date-timeのところは大文字にしています*8

System.out.printf("now: %s%n", LocalDateTime.now());

System.out.printf("Apr 15, 1994 @ 11:30am: %s%n",
                  LocalDateTime.of(1994, Month.APRIL, 15, 11, 30));

System.out.printf("now (from Instant): %s%n",
                  LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()));

System.out.printf("6 months from now: %s%n",
                  LocalDateTime.now().plusMonths(6));

System.out.printf("6 months ago: %s%n",
                  LocalDateTime.now().minusMonths(6));

このコードは以下のような出力となります。

now: 2013-07-24T17:13:59.985
Apr 15, 1994 @ 11:30am: 1994-04-15T11:30
now (from Instant): 2013-07-24T17:14:00.479
6 months from now: 2014-01-24T17:14:00.480
6 months ago: 2013-01-24T17:14:00.481

Time Zone and Offset Classes

タイムゾーン(time zone)とは、同一の標準時間が使用されている地球の地域、のことです。各タイムゾーンは識別子によって指定され、通常はregion/city(Asia/Tokyo)のフォーマットで、Greenwich/UTCからのオフセットを持ちます。たとえば、Tokyoのオフセットは+09:00です。

ZoneId and ZoneOffset

Date-Time APIには、タイムゾーンもしくはオフセットを指定するためのクラスが二つがあります。

  • ZoneIdタイムゾーン識別子で、InstantLocalDateTime間の変換ルールを提供します。
  • LocalDateTimeはGreenwich/UTCからのタイムゾーンオフセットを指定します。

Greenwich/UTCからのオフセットは時間単位*9で定義されますが、例外があります。以下のコードはTimeZoneIdの例からの抜粋で、Greenwich/UTCからのオフセットを使用しているすべてのタイムゾーンのうち、時間単位で定義されていないものをリストアップするものです。

Set<String> allZones = ZoneId.getAvailableZoneIds();
LocalDateTime dt = LocalDateTime.now();

// Create a List using the set of zones and sort it.
List<String> zoneList = new ArrayList<String>(allZones);
Collections.sort(zoneList);

...

for (String s : zoneList) {
    ZoneId zone = ZoneId.of(s);
    ZonedDateTime zdt = dt.atZone(zone);
    ZoneOffset offset = zdt.getOffset();
    int secondsOfHour = offset.getTotalSeconds() % (60 * 60);
    String out = String.format("%35s %10s%n", zone, offset);

    // 時間単位オフセットを持たないタイムゾーンのみを標準出力する。
    if (secondsOfHour != 0) {
        System.out.printf(out);
    }
    ...
}

この例は以下のリストを出力します。

      America/Caracas     -04:30
     America/St_Johns     -02:30
        Asia/Calcutta     +05:30
         Asia/Colombo     +05:30
           Asia/Kabul     +04:30
       Asia/Kathmandu     +05:45
        Asia/Katmandu     +05:45
         Asia/Kolkata     +05:30
         Asia/Rangoon     +06:30
          Asia/Tehran     +04:30
   Australia/Adelaide     +09:30
Australia/Broken_Hill     +09:30
     Australia/Darwin     +09:30
      Australia/Eucla     +08:45
        Australia/LHI     +10:30
  Australia/Lord_Howe     +10:30
      Australia/North     +09:30
      Australia/South     +09:30
 Australia/Yancowinna     +09:30
  Canada/Newfoundland     -02:30
         Indian/Cocos     +06:30
                 Iran     +04:30
              NZ-CHAT     +12:45
      Pacific/Chatham     +12:45
    Pacific/Marquesas     -09:30
      Pacific/Norfolk     +11:30

また、上記のTimeZoneIdのコード例はすべてのタイムゾーンIDをファイルに出力します。

The Date-Time Classes

Date-Time APIは、タイムゾーンと共に動作する、3つのtemporal-basedクラスを提供します。

いつ、ZonedDateTimeの代わりにOffsetDateTimeを使えば良いのでしょうか? もし、地理的な位置情報に基づく日付と時間計算のために自前のルールをモデル化する複雑なソフトウェアを記述しようとしてたり、データベースにGreenwich/UTCからの絶対オフセットのみを記録するタイムスタンプ(time-stamps)を格納しようとしている場合、OffsetDateTimeを使用するのが良いでしょう。また、XMLその他の通信フォーマットはOffsetDateTimeもしくはOffsetTimeとしてdate-timeトランスファーをフォーマットします。

この三つのクラスはGreenwich/UTCからのオフセットを維持しますが、ZonedDateTimeのみ、java.time.zoneパッケージのZoneRulesを使用して、どのようにオフセットが特定のタイムゾーンごとに異なるかを決定します。たとえば、多くのタイムゾーンは、時計をサマータイムに進めるとき(一般的には1時間)にはギャップを生じ、そして、標準時に戻すとき最後の1時間は重なり合うことになります。ZonedDateTimeクラスはこうしたシナリオに適応するもので、OffsetDateTimeOffsetTimeクラスは、ZoneRulesにはアクセスしません。

ZonedDateTime

ZonedDateTimeクラスは、実質的には、LocalDateTimeクラスとZoneIdクラスを組み合わせたものです。このクラスは、日付(年・月・日)、時間(時・分・秒・ナノ秒)、タイムゾーンEurope/Parisのようなregion/city)、を表現するために使用します。

以下のコードは、Flight例からの抜粋で、サンフランシスコから東京への旅客機の出発時刻を、America/Los AngelesタイムゾーンZonedDateTimeで定義したものです。withZoneSameInstantplusMinutesメソッドZonedDateTimeインスタンスを生成するために使用するもので、このインスタンスは650分飛行後の東京予定到着時刻を表現しています。ZoneRules.isDaylightSavingsメソッドはフライトが東京に到着するときサマータイムかどうかを調べます。

DateTimeFormatterオブジェクトはZonedDateTimeインスタンスの出力をフォーマットするために使用します。

DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM d yyyy  hh:mm a");

//出発時刻: San Francisco on July 20, 2013, at 7:30 p.m.
LocalDateTime leaving = LocalDateTime.of(2013, Month.JULY, 20, 19, 30);
ZoneId leavingZone = ZoneId.of("America/Los_Angeles"); 
ZonedDateTime departure = ZonedDateTime.of(leaving, leavingZone);

try {
    String out1 = departure.format(format);
    System.out.printf("LEAVING:  %s (%s)%n", out1, leavingZone);
} catch (DateTimeException exc) {
    System.out.printf("%s can't be formatted!%n", departure);
    throw exc;
}

// フライトは10時間50分もしくは650分
ZoneId arrivingZone = ZoneId.of("Asia/Tokyo"); 
ZonedDateTime arrival = departure.withZoneSameInstant(arrivingZone)
                                 .plusMinutes(650);

try {
    String out2 = arrival.format(format);
    System.out.printf("ARRIVING: %s (%s)%n", out2, arrivingZone);
} catch (DateTimeException exc) {
    System.out.printf("%s can't be formatted!%n", arrival);
    throw exc;
}

if (arrivingZone.getRules().isDaylightSavings(arrival.toInstant())) 
    System.out.printf("  (%s daylight saving time will be in effect.)%n",
                      arrivingZone);
else
    System.out.printf("  (%s standard time will be in effect.)%n",
                      arrivingZone);

このコードは以下のような出力になります。

LEAVING:  Jul 20 2013  07:30 PM (America/Los_Angeles)
ARRIVING: Jul 21 2013  10:20 PM (Asia/Tokyo)
  (Asia/Tokyo standard time will be in effect.)

OffsetDateTime

OffsetDateTimeクラスは、実質的に、LocalDateTimeクラスとZoneOffsetクラスを組み合わせたものです。このクラスを使用して表現できるのは、日付(年・月・日)と時間(時・分・秒・ナノ秒)とGreenwich/UTCからのオフセット(+06:00-08:00のような、+/-hh:mm)です。

以下の例は、2013年7月の最後の木曜日を取得するために、TemporalAdjuster.lastDayメソッドと共にOffsetDateTimeを使用しています。

// 2013年7月最終木曜日を取得する。
LocalDateTime date = LocalDateTime.of(2013, Month.JULY, 20, 19, 30);
ZoneOffset offset = ZoneOffset.of("-08:00");
OffsetDateTime date = OffsetDateTime.of(date, offset);
OffsetDateTime lastThursday =
        date.with(TemporalAdjuster.lastInMonth(DayOfWeek.THURSDAY));
System.out.printf("The last Thursday in July 2013 is the %sth.%n",
                   lastThursday.getDayOfMonth());

このコードの実行結果は以下になります。

The last Thursday in July 2013 is the 25th.

OffsetTime

OffsetTimeクラスは、実質的には、LocalTimeクラスとZoneOffsetクラスを組み合わせたものです。このクラスを使用して表現できるのは、時間(時・分・秒・ナノ秒)とGreenwich/UTCからのオフセット(+06:00-08:00のような、+/-hh:mm)です。

OffsetTimeクラスはOffsetDateTimeクラスと同様な状況で使用出来ますが、こちらは日付を持つ必要が無いときに使用します。

Instant Class

Date-Time APIのコアクラスの一つがInstantクラスで、時間の流れ(timeline)における、ある地点からの経過ナノ秒を表現するものです。このクラスはmachine timeを表現するタイムスタンプの生成に使用できます。

import java.time.Instant;

Instant timestamp = Instant.now();

Instantクラスが返す値は、EPOCHとも呼ばれる1970年1月1日(1970-01-01T00:00:00Z)を起点としてカウントするものです。エポック以前のinstantは負の値となり、エポック以降のinstantは正の値となります。

Instantクラスで提供されるその他の定数値にはMINがあり、instantが取り得る最小(遥か過去)の値を意味し、MAXは最大(遠い未来)のinstantの値を意味します。

InstanttoStringを呼び出すと以下のような出力になります。

2013-05-30T23:38:23.085Z

このフォーマットは日付と時間を表現するISO-8601標準に従うものです。

Instantクラスは、Instantを操作するためのメソッドを豊富に備えています。plusminusメソッドで時間の加算・減算ができます。以下のコードは現在時刻に1時間を加算します。

Instant oneHourLater = Instant.now().plusHours(1);

instantの比較のためにはisAfterisBeforeメソッドが存在します。untilメソッドは二つのInstantオブジェクト間にどのくらいの時間差があるかを返します。以下のコード例は、Javaエポックの起点から現在時刻までの差を返します。

long secondsFromEpoch = Instant.ofEpochSecond(0L).until(Instant.now(),
                        ChronoUnit.SECONDS);

Instantクラスは、年・月・日などのhuman timeの単位のようには動作しません。もし、そうした単位で計算が必要な場合、Instantを別のLocalDateTimeZonedDateTimeクラスなどに、タイムゾーンをバインドし、変換することができます。以下のコードは、ofInstantメソッドを使用してInstantLocalDateTimeオブジェクトへデフォルトのタイムゾーンで変換し、読みやすい形式で日付と時間を出力しています。

Instant timestamp;
...
LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault());
System.out.printf("%s %d %d at %d:%d%n", ldt.getMonth(), ldt.getDayOfMonth(),
                  ldt.getYear(), ldt.getHour(), ldt.getMinute());

出力は以下のような結果になります。

MAY 30 2013 at 18:21

ZonedDateTimeOffsetTimeZoneオブジェクトも、時間の流れにおけるある瞬間を示すInstantオブジェクトへと変換できます。しかし、逆は真ではありません。InstantオブジェクトをZonedDateTimeOffsetDateTimeに変換するには、タイムゾーンタイムゾーンオフセットの情報が必要です。

Parsing and Formatting

Date-Time APIのtemporal-basedクラスは、日付と時間の情報を含む文字列をパースするためのparseメソッドを持っています。また、これらのクラスはtemporal-basedオブジェクトをフォーマットして表示するためのformatメソッドも持っています。双方とも使い方は似ています。formatterオブジェクトを生成するには、DateTimeFormatterへパターンを渡します。このformatterはparseformatメソッドの引数となります。

DateTimeFormatterクラスは、多数の定義済formatterを持っており、あるいは、自前のクラスを定義できます。

parseformatメソッドは変換処理中に問題が発生した場合に例外をスローします。そのため、パースするコードはDateTimeParseException、フォーマットはDateTimeExceptionをキャッチすべきです。より詳細な例外処理の情報については、Catching and Handling Exceptionsを参照してください。

DateTimeFormatterクラスはイミュータブルかつスレッドセーフです。必要に応じてstatic定数として宣言できます(あるいは、そうすべきです)。

Version Note:java.timedate-timeオブジェクトは、java.util.Formatterとレガシーなjava.util.Datejava.util.Calendarで使われていたなじみ深いパターンベースフォーマットによるString.format、を直接使用することができます。

Parsing

LocalDateクラスの単一の引数を持つparse(CharSequence)メソッドISO_LOCAL_DATEformatterを使用します。異なるformatterを指定するには、二つの引数を持つparse(CharSequence, DateTimeFormatter)メソッドを使用します。以下の例は定義済みのBASIC_ISO_DATEformatterを使用して、19590709のフォーマットを1959年7月9日にパースします。

String in = ...;
LocalDate date = LocalDate.parse(in, DateTimeFormatter.BASIC_ISO_DATE);

自前のパターンを使用するformatterを定義することもできます。以下のコードは、Parseの例の抜粋で、"MMM d yyyy"のフォーマットを適用するformatterを生成しています。このフォーマットは、3文字の月・1文字の日・4文字の年、を表現しています。このパターンを使用して生成されたformatterは"Jan 3 2003"や"Mar 23 1994"のような文字列を受け入れます。しかし、"MMM dd yyyy"のように日を二文字のフォーマットを指定すると、"Jun 03 2003"のようにゼロパディングをして日を常に二文字で指定しなければなりません。

String input = ...;
try {
    DateTimeFormatter formatter =
                      DateTimeFormatter.ofPattern("MMM d yyyy");
    LocalDate date = LocalDate.parse(input, formatter);
    System.out.printf("%s%n", date);
}
catch (DateTimeParseException exc) {
    System.out.printf("%s is not parsable!%n", input);
    throw exc;      // Rethrow the exception.
}
// 'date' has been successfully parsed

DateTimeFormatterクラスのドキュメントには、フォーマットやパースのパターンに指定可能なすべてのシンボルのリストが定義されています。

Non-ISO Date ConversionページのStringConverterの例はdate formatterの別の例を示しています。

Formatting

format(DateTimeFormatter)メソッドは、temporal-basedオブジェクトを、指定されたフォーマットを使用して文字列へと変換します。以下のコードは、Flightサンプルの抜粋で、ZonedDateTimeインスタンスを"MMM d yyy hh:mm a"フォーマットを使用して変換しています。日付はパースの例で使用したのと同様な方法で定義されていますが、このパターンは時・分・a.m./p.m要素を含んでいます。

ZoneId leavingZone = ...;
ZonedDateTime departure = ...;

try {
    DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM d yyyy  hh:mm a");
    String out = departure.format(format);
    System.out.printf("LEAVING:  %s (%s)%n", out, leavingZone);
}
catch (DateTimeException exc) {
    System.out.printf("%s can't be formatted!%n", departure);
    throw exc;
}

この例の出力は、以下のように出発と到着時刻を表示します。

LEAVING:  Jul 20 2013  07:30 PM (America/Los_Angeles)
ARRIVING: Jul 21 2013  10:20 PM (Asia/Tokyo)

The Temporal Package

java.time.temporalパッケージは、インターフェース、クラス、日付と時間のコードをサポートするenum、日付と時間計算、を提供します。

これらのインタフェースは最も低いレベルで使用されることを意図しています。一般的なアプリケーションコードは、LocalDateZonedDateTimeのような、変数と特定の型の引数を宣言すべきですが、Temporalインタフェースはそうではありません。同様なこととしては、Stringは変数を宣言しますが、CharSequenceはそうではありません。

Temporal and TemporalAccessor

Temporalインタフェースはtemporal-basedオブジェクトにアクセスするためのフレームワークを提供し、また、このインタフェースはInstant, LocalDateTime, ZonedDateTimeなどのtemporal-basedクラスによって実装されています。このインタフェースは単位時間の加算・減算をするためのメソッドを提供しており、これらは様々な日付と時間のクラスに一貫性を保ち、時間ベースの算術が容易になります。TemporalAccessorインタフェースはTemporalインタフェースのリードオンリーバージョンです。

TemporalTemporalAccessorオブジェクトは、TemporalFieldインタフェースで指定される一単位としてのフィールドとして定義されています。ChronoField enumTemporalFieldインタフェースの実装で、DAY_OF_WEEK, MINUTE_OF_HOUR, MONTH_OF_YEARのような、定数値のセットを提供します。

それらのフィールドのための単位(units)はTemporalUnitインターフェースによって定義されます。ChronoUnit enumTemporalUnitインタフェースを実装します。ChronoField.DAY_OF_WEEKフィールドはChronoUnit.DAYSChronoUnit.WEEKSの組み合わせです。ChronoFieldChronoUnit enumは以降のセクションで扱います。

Temporalインタフェースの算術ベース(arithmetic-based)メソッドTemporalAmountを単位として定義されるパラメータを要求します。PeriodDurationクラスがTemporalAmountインタフェースを実装しています(Period and Duration)で解説します)。

ChronoField and IsoFields

ChronoField enumTemporalFieldインタフェースを実装しており、日付と時間の値を取得するための定数値を提供しています。CLOCK_HOUR_OF_DAY, NANO_OF_DAY, DAY_OF_YEARの例をいくつか示します。このenumは時間の概念的な捉え方を表現するために使用でき、たとえば、ある年の第三週、11時*11、ある月の最初の月曜日、などが挙げられます。良く知らないTemporalに遭遇した場合、TemporalAccessor.isSupported(TemporalField)メソッドを使用して、個々のフィールドをTemporalがサポートするかどうか判定できます。以下のコードはfalseを返し、LocalDateChronoField.CLOCK_HOUR_OF_DAYをサポートしないことを意味しています。

boolean isSupported = LocalDate.now().isSupported(ChronoField.CLOCK_HOUR_OF_DAY);

ISO-8601カレンダーシステムに固有の追加フィールドがIsoFieldsに定義されています。以下の例はChronoFieldIsoFieldsの両方を使用するフィールド値を取得する方法を示しています。

time.get(ChronoField.MILLI_OF_SECOND)
int qoy = date.get(IsoFields.QUARTER_OF_YEAR);

二つの異なるクラスが役に立ちそうな追加的なフィールドを定義しており、それらはWeekFieldsJulianFieldsです。

ChronoUnit

ChronoUnit enumTemporalUnitインタフェースを実装するもので、ミリ秒から世紀までの日付と時間に基づく標準的な単位を提供します。注意点として、ChronoUnitオブジェクトはすべてのクラスでサポートされているわけではありません。たとえば、InstantクラスはChronoUnit.MONTHSChronoUnit.YEARSをサポートしません。TemporalAccessor.isSupported(TemporalField)*12メソッドで、あるクラスが個々の時間単位をサポートするかどうかを検証することができます。以下のisSupported呼び出しはfalseを返し、InstantクラスはChronoUnit.DAYSをサポートしないことを確認できます。

boolean isSupported = instant.isSupported(ChronoUnit.DAYS);

Temporal Adjuster

java.time.temporalパッケージのTemporalAdjusterインタフェースは、引数にTemporalを取り調整された値(adjusted value)を返します。adjustersは任意のtemporal-based型と共に使用します。

Predefined Adjusters

TemporalAdjustersクラス(複数形なことに注意)は定義済みのadjustersを提供しており、数例を上げると、ある月の最初の日や最後の日、ある年の最初の日や最後の日、ある月の最後の水曜日、指定日後での最初の火曜日、などが取得できます。定義済みadjustersはstaticメソッドとして定義されており、static importを使うように設計されています。

以下の例はTemporalAdjustersメソッドのいくつかを使用したもので、temporal-basedクラスに定義されたwithメソッドと共に使うことで、計算元の2000/10/15から新しい日付を算出しています。

LocalDate date = LocalDate.of(2000, Month.OCTOBER, 15);
DayOfWeek dotw = date.getDayOfWeek();
System.out.printf("%s is on a %s%n", date, dotw);

System.out.printf("first day of Month: %s%n",
                  date.with(TemporalAdjusters.firstDayOfMonth()));
System.out.printf("first Monday of Month: %s%n",
                  date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));
System.out.printf("last day of Month: %s%n",
                  date.with(TemporalAdjusters.lastDayOfMonth()));
System.out.printf("first day of next Month: %s%n",
                  date.with(TemporalAdjusters.firstDayOfNextMonth()));
System.out.printf("first day of next Year: %s%n",
                  date.with(TemporalAdjusters.firstDayOfNextYear()));
System.out.printf("first day of Year: %s%n",
                  date.with(TemporalAdjusters.firstDayOfYear()));

この例は以下の出力になります。

2000-10-15 is on a SUNDAY
first day of Month: 2000-10-01
first Monday of Month: 2000-10-02
last day of Month: 2000-10-31
first day of next Month: 2000-11-01
first day of next Year: 2001-01-01
first day of Year: 2000-01-01

Custom Adjusters

自前のカスタムadjusterを作ることもできます。そのためには、adjustInto(Temporal)メソッドを持つTemporalAdjusterインタフェースを実装するクラスを用意します。NextPayDayサンプルからの抜粋したPaydayAdjusterはカスタムadjusterです。PaydayAdjusterは引数の日付を評価して次の支払日を返します。支払日は月二回で毎月15日と月の最終日と仮定します。もし算出された日付が土日な場合は、直前の金曜日にします。年は今年*13と仮定します。

/**
 * The adjustInto method accepts a Temporal instance 
 * and returns an adjusted LocalDate. If the passed in
 * parameter is not a LocalDate, then a DateTimeException is thrown.
 */
public Temporal adjustInto(Temporal input) {
    LocalDate date = LocalDate.from(input);
    int day;
    if (date.getDayOfMonth() < 15) {
        day = 15;
    } else {
        day = date.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
    }
    date = date.withDayOfMonth(day);
    if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
        date.getDayOfWeek() == DayOfWeek.SUNDAY) {
        date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
    }

    return input.with(date);
}

このadjusterは定義済みadjusterと同様な方法で、withメソッドを使用するときに呼び出すことができます。以下のコード例はNextPaydayサンプルからのものです。

LocalDate nextPayday = date.with(new PaydayAdjuster());

2013年では、6月15日と6月30日は両方とも土日です。NextPaydayサンプルをそれぞれ2013年6月3日と2013年6月18日で実行すると、以下の結果が得られます。

Given the date:  2013 Jun 3
the next payday: 2013 Jun 14

Given the date:  2013 Jun 18
the next payday: 2013 Jun 28

Temporal Query

TemporalQueryはtemporal-basedオブジェクトから情報を検索するために使用します。

Predefined Queries

TemporalQueries(複数形なことに注意)クラスはいくつかの定義済みクエリーを提供しており、これにはアプリケーションがtemporal-basedオブジェクトの型を指定できないときに役立つメソッドが含まれています。adjustersと同様に、定義済みクエリーはstaticメソッドとして定義されており、static importを使うように設計されています。

precisionクエリーは、例えば、特定のtemporal-basedオブジェクトが返すことのできる最小のChronoUnitを返します。以下の例はprecisionクエリーをいくつかのtemporal-basedオブジェクトの型に対して使用しています。

TemporalQueries query = TemporalQueries.precision();
System.out.printf("LocalDate precision is %s%n",
                  LocalDate.now().query(query));
System.out.printf("LocalDateTime precision is %s%n",
                  LocalDateTime.now().query(query));
System.out.printf("Year precision is %s%n",
                  Year.now().query(query));
System.out.printf("YearMonth precision is %s%n",
                  YearMonth.now().query(query));
System.out.printf("Instant precision is %s%n",
                  Instant.now().query(query));

出力は以下のようになります。

LocalDate precision is Days
LocalDateTime precision is Nanos
Year precision is Years
YearMonth precision is Months
Instant precision is Nanos

Custom Queries

自前のカスタムクエリーを作ることもできます。その方法は、TemporalQueryインタフェースを実装してqueryFrom(TemporalAccessor) を作成することです。CheckDateサンプルは二つのカスタムクエリを実装しています。一つ目のカスタムクエリはFamilyVacationsクラスで、TemporalQueryインタフェースを実装しています。queryFromメソッドは渡された日付と休暇スケジュールを比較し、もし範囲内であればTRUEを返します。

// 渡された日付が家族の休暇日に含まれていればtrueを返します。
// クエリは月日のみ比較するので、Temporal型が異なっていても
// チェックは成功します。
public Boolean queryFrom(TemporalAccessor date) {
    int month = date.get(ChronoField.MONTH_OF_YEAR);
    int day   = date.get(ChronoField.DAY_OF_MONTH);

    // 春休みのディズニーランド
    if ((month == Month.APRIL.getValue()) && ((day >= 3) && (day <= 8)))
        return Boolean.TRUE;

    // ソーガタック湖でのスミス家親睦会
    if ((month == Month.AUGUST.getValue()) && ((day >= 8) && (day <= 14)))
        return Boolean.TRUE;

    return Boolean.FALSE;
}

*14

次のカスタムクエリーはFamilyBirthdaysクラスで実装されているものです。このクラスはisFamilyBirthdayメソッドを提供しており、渡された日付といくつかの誕生日を比較し、もし一致すればTRUEを返します。

// 渡された日付が家族の誕生日のいずれか一つと一致すればtrueを返します。
// クエリは月日のみ比較するので、Temporal型が異なっていても
// チェックは成功します。
public static Boolean isFamilyBirthday(TemporalAccessor date) {
    int month = date.get(ChronoField.MONTH_OF_YEAR);
    int day   = date.get(ChronoField.DAY_OF_MONTH);

    // Angie's birthday is on April 3.
    if ((month == Month.APRIL.getValue()) && (day == 3))
        return Boolean.TRUE;

    // Sue's birthday is on June 18.
    if ((month == Month.JUNE.getValue()) && (day == 18))
        return Boolean.TRUE;

    // Joe's birthday is on May 29.
    if ((month == Month.MAY.getValue()) && (day == 29))
        return Boolean.TRUE;

    return Boolean.FALSE;
}

FamilyBirthdayクラスはTemporalQueryインターフェースを実装していませんが、ラムダ式の一部として使用できます。以下のコードは、CheckDateサンプルからのもので、二つのカスタムクエリの呼び出し方を示しています。

// ラムダ式を使用しないクエリの呼び出し
Boolean isFamilyVacation = date.query(new FamilyVacations());

// ラムダ式を使用したクエリの呼び出し
Boolean isFamilyBirthday = date.query(FamilyBirthdays::isFamilyBirthday);

if (isFamilyVacation.booleanValue() || isFamilyBirthday.booleanValue())
    System.out.printf("%s is an important date!%n", date);
else
    System.out.printf("%s is not an important date.%n", date);

Period and Duration

時間の合計を指定するコードを書こうとするとき、最良のクラスとメソッドを、Durationクラス、Periodクラス、ChronoUnit.between メソッド、などに見出せるでしょう。Durationは時間ベース(time-based)の値(秒、ナノ秒)を使用して時間の合計を計測します。Periodは日付ベース(date-based)の値(年、月、日)を使用します。

Note:一日のDurationは正確に(exactly)24時間のlongです。一日のPeriodは、ZonedDateTimeが追加されたとき、タイムゾーンに従って変化します。たとえば、サマータイムの最初と最後の日では変化します。

Duration

Durationは、Instantオブジェクトを使用するコードのような、machine-based時間を計測する状況で最も役立ちます。Durationオブジェクトは秒あるいはナノ秒で計測され、日数・時間・分を変換するメソッドは持ちますが、年・月・日のような日付ベースで生成する仕組みは持ちません。Durationはマイナス値を持つことができ、これは開始ポイントの前に終了ポイントを指定する場合に発生します。

以下のコードでは、ナノ秒で、二つのinstant間の時間を計算します。

Instant t1, t2;
...
long ns = Duration.between(t1, t2).toNanos();

以下のコードはInstantに10秒を足します。

Instant start;
...
Duration gap = Duration.ofSeconds(10);
Instant later = start.plus(gap);

Durationは実世界の時間とは切り離されており、従ってタイムゾーンサマータイムを追従しません。一日と等しいDurationZonedDateTimeに加えることは、サマータイムやその他の異なる結果を返す時差かどうかにかかわらず、24時間を加えることと正確に等しいです。

ChronoUnit

ChronoUnit enumは、The Temporal Packageで説明したように、時間計測のためのユニットを定義しています。ChronoUnit.betweenメソッドは、日数や秒数のような、時間単位のみで合計を計測する場合に役立ちます。betweenメソッドはすべてのtemporal-basedオブジェクトで動作しますが、単一単位での合計のみ返します。以下のコードは、二つのタイムスタンプ間のミリ秒でのギャップを計算しています。

import java.time.Instant;
import java.time.temporal.Temporal;
import java.time.temporal.ChronoUnit;

Instant previous, current, gap;
...
current = Instant.now();
if (previous != null) {
    gap = ChronoUnit.MILLIS.between(previous,current);
}
...

Period

日付ベース値(年・月・日)で時間の合計を定義するには、Periodクラスを使用します。Periodクラスは様々なgetメソッドを提供しており、たとえばgetMonthsgetDaysgetYearsなどで、これらを使用することでピリオドから時間の合計を抽出できます。

時間の総合計(total period of time)は年・月・日の三つの単位によって表現されます。日数のような単一の単位で計測される時間の合計を表現するには、ChronoUnit.betweenメソッドを使用します。

以下のコードは、あなたが1960年1月1日に生まれたと仮定して、年齢を計算します。Periodクラスは年・月・日単位で計算を行うために使用します。合計日数は、同一のピリオドをChronoUnit.betweenメソッドを使用することで決定でき、括弧内に表示しています。

LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1960, Month.JANUARY, 1);

Period p = Period.between(birthday, today);
long p2 = ChronoUnit.DAYS.between(birthday, today);
System.out.println("You are " + p.getYears() + " years, " + p.getMonths() +
                   " months, and " + p.getDays() +
                   " days old. (" + p2 + " days total)");

このコードは以下のような出力になります。

You are 53 years, 4 months, and 29 days old. (19508 days total)

次の誕生日までの日数を計算するには、Birthdayサンプルの以下のコードのようになります。Periodクラスは月数と日数を計算するのに使用します。ChronoUnit.betweenメソッドは合計日数を返すので、それを括弧内に表示しています。

LocalDate birthday = LocalDate.of(1960, Month.JANUARY, 1);

LocalDate nextBDay = birthday.withYear(today.getYear());

//もし今日が今年の誕生日以降なら、1年足す。
if (nextBDay.isBefore(today) || nextBDay.isEqual(today)) {
    nextBDay = nextBDay.plusYears(1);
}

Period p = Period.between(today, nextBDay);
long p2 = ChronoUnit.DAYS.between(today, nextBDay);
System.out.println("There are " + p.getMonths() + " months, and " +
                   p.getDays() + " days until your next birthday. (" +
                   p2 + " total)");

このコード以下のような結果になります。

There are 7 months, and 2 days until your next birthday. (216 total)

これらの計算はタイムゾーンの違いを解釈しません。たとえば、もしあなたがオーストラリアで生まれて、現在はバンガロール*15在住の場合、実際の年齢計算にわずかな影響を与えます。このような場合では、ZonedDateTimeクラスと共にPeriodを使用します。PeriodZonedDateTimeを加えるとき、時差が考慮されます。

Clock

多くのtemporal-basedオブジェクトは引数無しのnow()メソッドを備えており、このメソッドはシステムクロックとデフォルトタイムゾーンを使用して現在の日付と時刻を返します。また、これらのtemporal-basedオブジェクトは一つの引数を受け取るnow(Clock)メソッドを備えており、これには別のClockを渡せます。

現在の日付と時刻は、国際化を必要とするアプリケーションでは、タイムゾーンに依存し、Clockは正しいタイムゾーンに基づいて日付/時刻が生成されることを保障する必要があります。Clockクラスの使用はオプションですが、この機能はあなたのコードを別のタイムゾーンや、時刻を固定した時計(fixed clock)で、テストできるようになります。

Clockクラスは抽象クラスなので、インスタンスを生成することは出来ません。以下のファクトリーメソッドがテストのために使用できます。

Non-ISO Date Conversion

このチュートリアルjava.time.chronoパッケージの詳細については解説しません。しかしながら、いくつかの非ISOベースの定義済みの暦、たとえば和暦、ヒジュラ(Hijrah)、民国紀元(Minguo)*16仏滅紀元(Thai Buddhist)を知っておくのは時に有用となるでしょう。このパッケージを使用して環境ごとの暦を使用することができます。

このセクションはISOベースの日付とその他の定義済みの暦との変換方法について示します。

Converting to a Non-ISO-Based Date

ISOベースの日付を別の暦に変更するには、JapaneseDate.from(TemporalAccessor)などのfrom(TemporalAccessor)メソッドを使用します。このメソッドは、もし日付を妥当なインスタンスに変換できない場合はDateTimeExceptionをスローします。以下のコードはLocalDateTimeインスタンスをいくつかの定義済み非ISOカレンダーの日付に変換しています。

LocalDateTime date = LocalDateTime.of(2013, Month.JULY, 20, 19, 30);
JapaneseDate jdate     = JapaneseDate.from(date);
HijrahDate hdate       = HijrahDate.from(date);
MinguoDate mdate       = MinguoDate.from(date);
ThaiBuddhistDate tdate = ThaiBuddhistDate.from(date);

StringConverterサンプルでは、LocalDateChronoLocalDateに変換したあとStringを返します。toStringメソッドは引数にLocalDateChronologyインスタンスを取り、引数Chronologyを使用して変換した文字列を返します。DateTimeFormatterBuilderは、日付表示のための文字列を組み立てるために使用されています。

/**
 * 引数のChronologyを使用してLocalDate (ISO)をChronoLocalDateに変換
 * してから、Chronologyと現在のロケールに基づく短いパターンのDateTimeFormatterを
 * 使用して、ChronoLocalDateを文字列にフォーマットします。
 *
 * @param localDate - このISOベースの日付を変換してフォーマットします。
 * @param chrono - オプションの暦。nullの場合はIsoChronologyを使用します。
 */
public static String toString(LocalDate localDate, Chronology chrono) {
    if (localDate != null) {
        Locale locale = Locale.getDefault(Locale.Category.FORMAT);
        ChronoLocalDate cDate;
        if (chrono == null) {
            chrono = IsoChronology.INSTANCE;
        }
        try {
            cDate = chrono.date(localDate);
        } catch (DateTimeException ex) {
            System.err.println(ex);
            chrono = IsoChronology.INSTANCE;
            cDate = localDate;
        }
        DateTimeFormatter dateFormatter =
            DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                             .withLocale(locale)
                             .withChronology(chrono)
                             .withDecimalStyle(DecimalStyle.of(locale));
        String pattern = "M/d/yyyy GGGGG";
        return dateFormatter.format(cDate);
    } else {
        return "";
    }
}

定義済みの暦でメソッドを呼び出すのは下記のようになります。

LocalDate date = LocalDate.of(1996, Month.OCTOBER, 29);
System.out.printf("%s%n",
     StringConverter.toString(date, JapaneseChronology.INSTANCE));
System.out.printf("%s%n",
     StringConverter.toString(date, MinguoChronology.INSTANCE));
System.out.printf("%s%n",
     StringConverter.toString(date, ThaiBuddhistChronology.INSTANCE));
System.out.printf("%s%n",
     StringConverter.toString(date, HijrahChronology.INSTANCE));

出力は以下のようになります。

10/29/0008 H
10/29/0085 1
10/29/2539 B.E.
6/16/1417 1

Converting to an ISO-Based Date

非ISOベースの日付をLocalDate.fromstatic メソッドを使用してLocalDateインスタンスに変換でき、以下がそのサンプルコードになります。

LocalDate date = LocalDate.from(JapaneseDate.now());

また、他のtemporal-basedクラスもこのメソッドを提供しており、もし変換不能な場合にはDateTimeExceptionをスローします。

StringConverterサンプルから抜粋したfromStringメソッドは、非ISOベースの日付を含むStringをパースしてLocalDateインスタンスを返します。

/**
 * 現在のロケールと引数の暦に基づく短いパターンのDateTimeFormatterを
 * 使用して文字列をChronoLocalDateにパースしたあと、LocalDate (ISO)に変換します。
 *
 * @param text   - 現在のロケールと引数のChronologyで解釈可能なフォーマット
 * の文字列を入力値として渡します。
 *
 * @param chrono - オプションのChronology。もしnullならIsoChronologyを使用します。
 */
public static LocalDate fromString(String text, Chronology chrono) {
    if (text != null && !text.isEmpty()) {
        Locale locale = Locale.getDefault(Locale.Category.FORMAT);
        if (chrono == null) {
           chrono = IsoChronology.INSTANCE;
        }
        String pattern = "M/d/yyyy GGGGG";
        DateTimeFormatter df = new DateTimeFormatterBuilder().parseLenient()
                              .appendPattern(pattern)
                              .toFormatter()
                              .withChronology(chrono)
                              .withDecimalStyle(DecimalStyle.of(locale));
        TemporalAccessor temporal = df.parse(text);
        ChronoLocalDate cDate = chrono.date(temporal);
        return LocalDate.from(cDate);
    }
return null;
}

以下のような文字列でこのメソッドを呼び出すことが出来ます。

System.out.printf("%s%n", StringConverter.fromString("10/29/0008 H",
    JapaneseChronology.INSTANCE));
System.out.printf("%s%n", StringConverter.fromString("10/29/0085 1",
    MinguoChronology.INSTANCE));
System.out.printf("%s%n", StringConverter.fromString("10/29/2539 B.E.",
    ThaiBuddhistChronology.INSTANCE));
System.out.printf("%s%n", StringConverter.fromString("6/16/1417 1",
    HijrahChronology.INSTANCE));

すべて1996年10月29日に変換された結果の文字列が表示されます。

Legacy Date-Time Code

Java SE 8 releaseより以前では、Javaの日付と時間のメカニズムは、java.util.Datejava.util.Calendarのサブクラスであるjava.util.GregorianCalendar、によって提供されてきました。これらのクラスは以下のようないくつかの欠点を抱えていました。

  • Calendarがタイプセーフでない。
  • クラスがミュータブルなので、マルチスレッドアプリケーションで使用できない。
  • アプリケーションコードのバグの原因は共通して奇妙な月のナンバリングと型安全の不足によるものです。

Interoperability with Legacy Code

おそらく大抵の人はjava.utilの日付と時間のクラスを使うレガシーコードを持っており、最小の手間でレガシーコードをjava.timeの機能を生かせるように変更したいことでしょう。

JDK 8リリースにはjava.utiljava.timeオブジェクト間の変更を行うメソッドが追加されています。

以下の例はCalendarインスタンスZonedDateTimeインスタンスに変換します。注意点として、InstantからZonedDateTimeの変換にはタイムゾーンが必須です。

Calendar now = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()));

以下の例はDateInstant間の変換を示しています。

Instant inst = date.toInstant();

Date newDate = Date.from(inst);

以下の例はGregorianCalendarからZonedDateTimeへの変換をしたあと、ZonedDateTimeからGregorianCalendarに変換しています。その他のtemporal-basedクラスはZonedDateTimeインスタンスを使用して生成されています。

GregorianCalendar cal = ...;

TimeZone tz = cal.getTimeZone();
int tzoffset = cal.get(Calendar.ZONE_OFFSET);

ZonedDateTime zdt = cal.toZonedDateTime();

GregorianCalendar newCal = GregorianCalendar.from(zdt);

LocalDateTime ldt = zdt.toLocalDateTime();
LocalDate date = zdt.toLocalDate();
LocalTime time = zdt.toLocalTime();

Mapping java.util Date and Time Functionality to java.time

Java SE 8リリースでは日付と時間の実装は完全に再設計されたので、あるメソッドを別のメソッドへと単純に入れ替えすることは出来ません。もしjava.timeパッケージが提供する豊富な機能を使いたい場合、最も簡単な解決策は以下のセクションでリストアップしているtoInstanttoZonedDateTimeメソッドを使用することです。しかし、このアプローチを望まない場合や、ニーズを満たさない場合には、日付と時間を扱うコードを書き直さなければなりません。

java.timeクラスがニーズに合うかどうかを評価し始めるには、Overviewページで紹介した表を起点にするのが良いでしょう。

二つのAPI間は一対一にマッピングできませんが、以下の表にjava.utilの日付と時間のクラスの機能をjava.timeAPIマッピングする際の一般的な考え方を示します。

java.util Functionality java.time Functionality Comments
java.util.Date java.time.Instant InstantDateクラスは似た物同士です。各クラスは、
・時間軸(UTL)上におけるある一瞬を表現する。
タイムゾーンの独立した時間を持つ。
ナノ秒を足したエポック秒(1970-01-01T00:00:00Z以降)として表現される。
Date.from(Instant)Date.toInstant()メソッドが両クラス間の変換を担当します。
java.util.GregorianCalendar java.time.ZonedDateTime ZonedDateTimeクラスはGregorianCalendarを置き換えるもので、以下のような似た機能を有しています。
human timeの表現は以下のように行います。
 LocalDate: 年、月、日
 LocalTime:時、分、秒、ナノ秒
 ZoneId タイムゾーン
ZoneOffset:GMTからの現在のオフセット
GregorianCalendar.from(ZonedDateTime)GregorianCalendar.to(ZonedDateTime)メソッドが両クラス間の変換を調整します。
java.util.TimeZone java.time.ZoneIdもしくはjava.time.ZoneOffset ZoneIdクラスはタイムゾーン識別子を指定し、使用する各タイムゾーンのルールにアクセスできます。ZoneOffsetクラスはGreenwich/UTCからのオフセットにみを指定します。より詳細な情報については、Time Zone and Offset Classesを参照してください。
1970-01-01GregorianCalendar java.time.LocalTime timeコンポーネントを使用するには、GregorianCalendarインスタンスに1970-01-01をセットするコードを、LocalTimeインスタンスで置き換えます。
00:00GregorianCalendar java.time.LocalDate dateコンポーネントを使用するには、GregorianCalendarに00:00をセットするコードを、LocalDateインスタンスで置き換えます。(このGregorianCalendarのアプローチは欠陥があります。国によってはサマータイム移行時に年一回00:00が発生しないためです。)

Date and Time Formatting

java.time.format.DateTimeFormatterは日付と時間のフォーマットのための強力なメカニズムを持ちますが、java.timeのtemporal-basedクラスをjava.util.FormatterString.formatで使うこともでき、これらはjava.utilの日付と時間クラスで使用しているパターンベースのフォーマットと同様なやり方が使えます。

Summary

java.timeパッケージは、日付と時間を表現するために使用可能な多くのクラスが含まれています。これらは豊富なAPIから成ります。ISOベースの日付のキー要素となるのは以下の通りです。

  • Instantクラスは時間軸上における機械時間表現(machine view)を提供します。
  • LocalDate, LocalTime, LocalDateTimeクラスは日付と時間の人的時間表現(human view)をタイムゾーン参照無しで提供します。
  • ZoneId, ZoneOffset, ZoneRulesクラスは、タイムゾーンタイムゾーンオフセット・タイムゾーンルールを定義します。
  • ZonedDateTimeクラスは日付と時間をタイムゾーン付きで表現します。OffsetDateTimeOffsetTimeクラスはそれぞれ、、日付と時間ないし時間、を表現します。これらのクラスはタイムゾーンオフセットをアカウントに取ります。
  • Durationクラスは秒とナノ秒単位での時間の合計を計測します。
  • Periodクラスは年・月・日単位での時間の合計を計測します。

他の非ISOカレンダーシステムはjava.time.chronoパッケージを使用して表現可能です。このパッケージはこのチュートリアルの範囲を超えてはいますが、Non-ISO Date ConversionページでISOベースの日付を別のカレンダーシステムに変換するための情報を提供しています。

Date Time APIはJSR 310の設計下でJava community processの一部として開発されました。詳細な情報については、JSR 310: Date and Time APIを参照してください。

Questions and Exercises: Date-Time API

(省略)

Date Time: End of Trail

"Date Time"トレイルはここで終わりです。

もしコメントや質問がある場合、フィードバックページ*17からおしらせください。

Core trail icon Essential Classes:こちらには文字列とプロパティについての情報があり、どちらも国際化対応プログラムで使用されます。

*1:イスラム教におけるヒジュラ

*2:仏滅紀元

*3:http://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar

*4:1582年以前の日付も扱える、という意味

*5:上手く訳せない…

*6:原文ではmonth-day-year

*7:詳細はぐぐって欲しいし俺も良くしらないかそういうヨットレースの大会があるらしい

*8:シンタックスハイライトの中を大文字にする方法ワカランのでやってないです

*9:whole hours

*10:このへん大変日本語がビミョーなので原文を参照願いたい…

*11:原文はthe 11th hour of the dayで、11th hourで最後の~とかぎりぎりの~などの意味があるようなので、単なる11時以上の意味がある気がする

*12:TemporalUnitを引数に取るメソッドが無いけど、何がどう変わったかまでは調べてない

*13:2013年

*14:Lake Saugatuck - ソーガタック湖はミシガン湖のリゾート地らしい。海かよっていうくらいデカイらしいので、湖っていう字面以上のスケールと思われる

*15:インド南部・カルナータカ州の州都

*16:民国紀元(みんこくきげん、正体字中国語:民國紀元・民國紀年)は、中華民国が成立した1912年を紀元(元年)とする紀年法である。参照元http://ejje.weblio.jp/content/Minguo+calendar

*17:言うまでもなくOracleに対するものなので、俺の訳文エントリに対するものはこのページのコメント欄や https://twitter.com/kagamihoge からどうぞ