日本人のための Date and Time API Tips

この記事はJava Advent Calendar 2013の4日目です。前日は @grimrose さんの「FluentLeniumの紹介について」です。


今回は当初予定していたDate and Time API (JSR 310) のディープな入門を取りやめ、Date and Time APIのTips集としました。
(ブログ記事なのに想定されるボリュームが夏と冬の有明で頒布される薄い本に匹敵するなんて、どこかイカれているでしょう?)
一応、読者が日本人であることを想定し、理論的背景にはあまり興味がないものとして話を進めます。

ご紹介するTipsは、特に断りのない限り、JDK8 Early Accessに含まれる Date and Time API (java.time.*) と、JSR 310の開発者達が公開しているJDK7向けのJSR 310ライクなOSSライブラリ ThreeTen backport の両方で利用可能です。

1. LocalDate、LocalTime、LocalDateTimeの基本的な使い方

これらはjava.util.Dateに代わるクラスで、日付のみを保持するLocalDate、時刻のみを保持するLocalTime、日付と時刻を保持するLocalDateTimeに分かれています。目的に合わせて使い分けましょう。

LocalDate系のクラスはタイムゾーンや時差の情報を持たないため、常に現在のタイムゾーンと見なされます。日本国内で実行する場合は基本的にJSTまたはAsia/Tokyoで表されるタイムゾーン(UTC+09:00)が無条件に適用されます。

1.1. 現在の日付、時刻、日付と時刻の取得方法

いずれのクラスにもファクトリー・メソッド now が用意されており、これを用います。

LocalDate date = LocalDate.now();  // 今日の日付
LocalTime time = LocalTime.now();  // 現在時刻
LocalDateTime dateTime = LocalDateTime.now();  // 現在時刻(日付を含む)

1.2. 特定の日付、時刻、日付と時刻の取得方法

いずれのクラスにもファクトリー・メソッド of が用意されており、これを用います。

LocalDate date = LocalDate.of(2013, 12, 4);  // 2013年12月4日
LocalTime time = LocalTime.of(10, 30, 45);  // 10時30分45秒
LocalDateTime dateTime1 = LocalDateTime.of(2013, 12, 4, 10, 30, 45);  // 2013年12月4日 10時30分45秒
LocalDateTime dateTime2 = LocalDateTime.of(date, time);  // 2013年12月4日 10時30分45秒

of メソッドはオーバーロードされており、特にLocalTimeとLocalDateTimeでは精度に合わせて様々なシグネチャが用意されています。詳細はJavadocを参照してください。

1.3. 相互変換

LocalDate系の3クラスは相互変換のメソッドが用意されています。

// LocalDate -> LocalDateTime
LocalDate date1 = LocalDate.of(2013, 12, 4);
LocalDateTime dateTime1 = date1.atTime(10, 30, 45);

// LocalTime -> LocalDateTime
LocalTime time2 = LocalTime.of(10, 30, 45);
LocalDateTime dateTime2 = time2.atDate(LocalDate.of(2013, 12, 4));

// LocalDateTime -> LocalDate, LocalTime
LocalDateTime dateTime3 = LocalDateTime.of(2013, 12, 4, 10, 30, 45);
LocalDate date3 = dateTime3.toLocalDate();
LocalTime time3 = dateTime3.toLocalTime();

1.4. 日付演算

LocalDate系の3クラスは、いくつかの日付演算をサポートしています。ここではLocalDateの日付演算から、○日後・○日前を求める方法を紹介します。日本ではおそらく、この演算を使用する機会が最も多いでしょう。

LocalDate today = LocalDate.now();  // 今日の日付
LocalDate twoDaysAfter = today.plusDays(2L);  // 2日後
LocalDate threeDaysBefore = today.minusDays(3L);  // 3日前

その他に日付の比較・判定もサポートされています。パターンが多いのでコードは省略しますが、日付の前後関係、うるう年か否か、といったことがメソッド1つで判定できます。

2. 日付・時刻のフォーマット

Date and Time APIでは、ISO 8601準拠のフォーマットを採用しています。日付・時刻を表すクラスの toString メソッドでISO 8601形式の文字列を返すことが出来ます。

ISO 8601形式は、順序こそ年・月・日ですが、区切り文字がハイフンであるなど、日本国内で普及しているスラッシュ区切りと異なっています。本来は国際規格であるISO 8601形式に合わせるべきなのでしょうが、慣習を変えることはそう簡単には出来ません。

そのようなケースに合わせて、Date and Time APIではフォーマットのカスタマイズ機能を提供しています。そのクラスはDateTimeFormatterと呼ばれ、フォーマットに合わせたインスタンスを作成して format メソッドや parse メソッドの引数に渡して使用します。なお、DateTimeFormatter にはあらかじめ定義済みのフォーマットが用意されていますが、いずれも日本の慣習には合わないようです。DateTimeFormatterはスレッドセーフであるため、あらかじめ想定されるフォーマットを作成してストックしておくと良いかもしれません。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");

LocalDate date1 = LocalDate.of(2013, 12, 4);
String text1 = date1.format(formatter));
System.out.println(text1);  // -> "2013/12/04"

String text2 = "2013/12/04";
LocalDate date2 = LocalDate.parse(text2, formatter);
System.out.println(date2);  // -> "2013-12-04"

DateTimeFormatterは非常に高機能で、提供されているメソッドを駆使すると想定されるほとんどのフォーマット(例えば今まで実現が難しかった曜日の漢字表記など)を実現することが出来ます。また、DateTimeFormatterのインスタンス生成を容易にするため、DateTimeFomatterBuilderクラスも用意されています。

3. 和暦サポート

筆者は実務では金融機関のシステムを担当しているため、日付は原則として西暦で扱います。しかし、官公庁のシステムを担当している部署では法律により和暦の使用が義務づけられるケースも存在しています。Date and Time APIでは明治以降の和暦もサポートしています。明治より前(慶応まで)の元号はサポートされていませんが、これは国際的な取り決めで和暦の元号は明治以降のみ定義し、それぞれアルファベットを割り当てる(明治="M"、大正="T"、昭和="S"、平成="H")ことになっているためです。

Date and Time APIで和暦を扱う場合には、LocalDateの代わりにChronoLocalDateインタフェースの実装の1つであるJapaneseDateを使用します。JapaneseDateの使い方はLocalDateとほぼ同じです。大きな相違点としては、of メソッドのオーバーライドで西暦または元号のいずれかを選択できるようになっていることと、toString メソッドの戻り値に和暦が含まれることです。

/**
 * Japanese chronology sample for JDK8 (Date and Time API)
 */
public class JapaneseDateSample {
    public static void main(String...args) {
        JapaneseDate date1 = JapaneseDate.of(2013, 12, 4);
        System.out.println(date1);
        
        JapaneseDate date2 = JapaneseDate.of(JapaneseEra.HEISEI, 25, 12, 4);
        System.out.println(date2);
    }
}

このサンプルの実行結果は以下のようになります。

Japanese Heisei 25-12-04
Japanese Heisei 25-12-04

上記はJDK8のDate and Time APIの場合で、ThreeTen backportではソースの記述に多少の差異があります。Date and Time APIでは暦に関する部分にJDK8で追加されたinterfaceのデフォルト実装を用いていますが、JDK7ベースのThreeTen backportではそれが使用できないため、両者で実装が異なっていることによります。以下に上記サンプルコードをThreeTen backportで書き直したものを示しますが、出力結果は全く同一です。

/**
 * Japanese chronology sample for JDK7 (ThreeTen backport)
 */
public class JapaneseDateSample {
    public static void main(String...args) {
        JapaneseChronology chrono = JapaneseChronology.INSTANCE;
        JapaneseDate date1 = chrono.date(2013, 12, 4);
        System.out.println(date1);
        
        JapaneseDate date2 = chrono.date(JapaneseChronology.ERA_HEISEI, 25, 12, 4);
        System.out.println(date2);
    }
}

4. Date・Calendarとの相互変換

4.1. Dateとの相互変換

java.util.Date クラスは散々叩かれながらもしぶとく生き残っています。Date and Time APIが普及するまでの間、当面Dateは活躍するでしょうから、両者の変換ユーティリティは早めに用意しておくと良いでしょう。

JDK8ではjava.util.Dateクラスに久々の改修が入り、Date and Time APIのInstantのインポート・エクスポートが可能になりました。逆に言うと、java.util.DateとDate and Time APIを直接相互変換するメソッドは用意されていないことになります。

実のところ、LocalDateとLocalTimeはInstant値を持っていません。InstantとはEpoch (1970-01-01T00:00:00Z) からの経過時間(時間軸上の位置)を表すもので、時刻が特定されていないLocalDateや日付を持たないLocalTimeからは時間軸上のどこにいるのかを特定できないためです。一方でLocalDateTimeは toInstant メソッドが存在し、現在のロケールからUTCとの時差を算出しInstantを求めることが出来ます。

// Sample for JDK8 (Date and Time API)
// LocalDateTime -> Date
LocalDateTime dateTime = LocalDateTime.now();
Instant instant = dateTime.toInstant();
Date date = Date.from(instant);

// Date -> LocalDateTime
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.systemDefault);

ThreeTen backportの場合はDate側がInstantを受け付けませんので、Instantをlong値に変換して相互変換に使用します。

// Sample for JDK7 (ThreeTen backport)
// LocalDateTime -> Date
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zonedDatetime = dateTime.atZone(ZoneOffset.systemDefault());
long time = zonedDateTime.toInstant().toEpochMilli();
Date date = new Date(time);

// Date -> LocalDateTime
Date date = new Date();
Instant instant = Instant.ofEpochMilli(date.getTime());
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(instant, ZoneOffset.systemDefault);
LocalDateTime dateTime = offsetDateTime.toLocalDateTime();

LocalDate、LocalTimeはともにLocalDateTimeとの相互変換が可能であることは 1.3. 節にて触れましたので、これらを組み合わせれば相互変換のユーティリティを作成することは難しくないでしょう。

4.2. Calendarとの相互変換

Calendarとの相互変換は、Dateの場合よりも複雑です。というのも、JDK8のCalendarには toInstant メソッドが追加されていますが、Instantを受け入れるための手段が用意されていません。現実解としては、CalendarとDateが相互変換可能であることに注目して、LocalDateTimeとDateの相互変換と、DateとCalendarの相互変換の2段階に分ける方法が考えられます。

5. JAXBをDate and Time APIに対応させる

JAXBは最近人気を集めているJAX-RSの入出力インタフェースとして需要が伸びています。JAXBよりも後に制定されたDate and Time APIはJAXBによるXMLの日付型(xs:datetime)とのマッピングが定義されていません。幸い、JAXBには標準でサポートされていないデータ型をXMLのデータ型とマッピングさせる手段が用意されており、それを用いてLocalDateTimeをJAXBに対応させてみようとおもいます。なお、他のクラスについても似たような処理でJAXBに対応させることが可能ですので、興味のある方は挑戦してみてください。

package jp.coppermine.libkit.threeten.xml.bind;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.threeten.bp.LocalDateTime;
import org.threeten.bp.OffsetDateTime;
import org.threeten.bp.ZoneOffset;

/**
 * This is mapped {@link LocalDateTime} to an XML representation as
 * "xs:datetime".
 * 
 * @see XmlAdapter
 */
public class LocalDateTimeXmlAdapter extends
	XmlAdapter<XMLGregorianCalendar, LocalDateTime> {
    @Override
    public LocalDateTime unmarshal(XMLGregorianCalendar value) throws Exception {
	int timezone = value.getTimezone();
	ZoneOffset offset = ZoneOffset.ofTotalSeconds(timezone * 60);
	return OffsetDateTime.of(value.getYear(), value.getMonth(),
		value.getDay(), value.getHour(), value.getMinute(),
		value.getSecond(), value.getMillisecond() * 1_000_000, offset)
		.toLocalDateTime();
    }

    @Override
    public XMLGregorianCalendar marshal(LocalDateTime value) throws Exception {
	ZoneOffset offset = OffsetDateTime.now().getOffset();
	int timezone = offset.getTotalSeconds() / 60;
	return DatatypeFactory.newInstance().newXMLGregorianCalendar(
		value.getYear(), value.getMonthValue(), value.getDayOfMonth(),
		value.getHour(), value.getMinute(), value.getSecond(),
		value.getNano() / 1_000_000, timezone);
    }
}

6. JavaFX 8のDatePicker―Date and Time API 唯一の採用例

Date and Time APIはJava SE 8から標準で含まれるものの、Java SE/EE APIは(前述のJAXBなど特殊なケースを除き)全く追従できていない状態です。その中でJavaFX 8のDatePickerはFeature Fix直前に何とかLocalDateに対応することが出来ました。JSR 310では今後、Date and Time APIをJDBC/JPAなどに対応させることを表明していますが、具体的なスケジュールまでは明らかになっていません。

7. まとめ

今回はDate and Time APIの多数の機能から、日本人が使いたがるであろう機能をピックアップして、Tips形式でお伝えしました。本来は「時」の概念を押さえた上で取り組むとよりいっそう理解が深まるのですが、残念ながらそこは割愛させて頂きました。ThreeTen backportに関する差分情報も可能な限り掲載しているため、JDK7を使用していてすぐにはJDK8に移行できない場合にも対応できるようにしたつもりです。

Date and Time APIを規定したJSR 310は、2007年から作業が行われている歴史の長いAPIです。スペックリードはイギリスのJava ChampionでJoda-Timeの作者としても知られているStephen Colebourneと、ブラジルのJava ChampionのMichael Nascimento Santosで、JSR 310が正式にOpenJDK8への合流が決まってから、3人目のスペックリードとしてOracleのRoger Riggsが加わりました。

JSR 310の当初の目的は、評判の芳しくなかったDate、Calendar、DateFormat等を一掃するための新しい日付・時刻APIを開発することで、当初はDate等既存のクラスとの相互変換手段は一切用意しないほどの徹底ぶりでした。

また、JSR 310は「時」という概念に真摯に向き合い、できる限り現実の「時」を表現できるように設計を進めていました。しかし、現実世界の「時」は非常に複雑ですべてを表現するのは非常に困難でした。JSR 310がOpenJDK8入りが決定した際、新APIが守るべき規程のサイズを大幅に超過していたため、現実の「時」を表現するための機能はやむなく一掃され、無数にあった日付演算も大幅な整理がなされました。

筆者がJSR 310に参加して感じたことは、このプロジェクトはほとんどStephenひとりの意思によって進められていたことです。彼による独裁体制は時には瞑想することもありましたが(Stephenが自ら手がけたJoda-Timeを超えようとして、フレデリック・ブルックスが言うところの "Second System Syndrome" に陥っていた時期もありました)、逆にStephenの強力なリーダーシップにより、非常に難しいプロジェクトでありながら途中で空中分解することなく無事、Java SE 8入りを果たすことが出来ました。

Date and Time APIはまたスタート地点に立ったに過ぎません。これからの動向に注目してゆきたいところです。


明日は @kagamihoge さんです。