TimeZoneを扱う

国内で国内向けのアプリケーションを作っている限りは、タイムゾーンを意識することはほとんどありません。しかし、Google App Engine等を利用する場合、多国対応のアプリケーションを作るためにはタイムゾーンを正しく扱うことが必要不可欠です。というわけで、タイムゾーンをGAE/Slim3を使う場合に考慮すべき事をまとめました。

デフォルトのタイムゾーン

Javaの場合、システムで持つデフォルトのタイムゾーンがあり、普段は暗黙的に利用されています。開発環境とプロダクション環境ではデフォルトのタイムゾーンが異なります。

開発環境 +0900 Asia/Tokyo
プロダクション環境 +0000 UTC

※開発環境のタイムゾーンはシステムに依存します。

デフォルトのタイムゾーンを指定する

GAE上を使うけどアプリケーションは国内向けであるのであれば、デフォルトのタイムゾーンを変更するのが簡単な方法です。web.xmlのコンテキストパラメータに次のようにして設定します。

  <context-param>
    <param-name>javax.servlet.jsp.jstl.fmt.timeZone</param-name>
    <param-value>Asia/Tokyo</param-value>
  </context-param>

ここで指定したタイムゾーンSlim3のFrontControllerで読み込まれ、次のようにしてTimeZoneを取得できます。

TimeZone timeZone = TimeZoneLocator.get();

ただし、システムのデフォルトタイムゾーンが変更された訳ではないので注意してください。

TimeZoneとDateとCalendarとSimpleDateFormat

ここでJavaでよく扱う日付時刻関連のクラスを再確認します。
まず、java.util.Dateですが、このクラスはエポック(1970/1/1)からのミリ秒で時間を表します。Dateクラスにはタイムゾーンは関係ありません。以下のようにデフォルトのコンストラクタを利用すると、現在時刻のDateオブジェクトが生成されます。

Date now = new Date();

次に、java.util.Calendarクラスですが、このクラスは日付時刻を表すクラスです。日付や時刻はタイムゾーンによって異なりますので、タイムゾーンの影響を強く受ける事になります。次のようにファクトリーメソッドを利用すると、現在時刻のシステムデフォルトのタイムゾーンでのCalendarインスタンスが取得されます。

Calendar cal = Calendar.getInstance();

タイムゾーンを指定してCalendarのインスタンスを取得するには引数に指定します。

TimeZone timeZone = ...
Calendar cal = Calendar.getInstance(timeZone);

Calendarクラスを使う事で、日付や時刻に関する処理を自然に行う事ができます。また、getTimeメソッドを使う事でDateオブジェクトに変換できます。

//  1/1の00:00:00に設定する
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, 0);  // 月は0始まりなので注意
cal.set(Calendar.DAY_OF_MONTH, 1); 
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date date = cal.getTime();

Dateオブジェクトに変換したならば、タイムゾーンは関係ないことに注意してください。Asia/Tokyoの1/1 00:00は他のタイムゾーンでは異なるDateとなります。
最後に、SimpleDateFormatですが、このクラスは日付のテキストにフォーマットすることによく使われます。Dateオブジェクトを引数にしますが、タイムゾーンはデフォルトでは、システムのデフォルトタイムゾーンが利用されます。

Date date =  ....
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String text = sdf.format(date);

タイムゾーンを指定する場合は、setTimeZoneメソッドを使用します。

TimeZone timeZone = ...
Date date =  ....
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(timeZone);
String text = sdf.format(date);

日付をどう扱うか?

日付を扱う場合には、入力の日付と出力の日付、システムで保持するデータとしての日付の3つを意識する必要があります。プログラムの中ではその3つの変換をタイムゾーンを意識して行わなければなりません。普段、あまり問題とならないのは、「入出力とデータのタイムゾーンが一致しており、かつシステムのデフォルトタイムゾーンも一致している」ことが多いからです。しかし、Google App EngineでAsia/Tokyoのタイムゾーンを扱おうとした場合、システムのデフォルトのタイムゾーンが異なるために、タイムゾーンを意識しないとおかしな事になるわけです。内部データを標準時として扱うか、タイムゾーンを意識して扱うかでアプローチは変わってきます。通常はタイムゾーンを意識せずに標準時で扱う方がよいのですが、Datastoreではインデックスやフィルタの関係から、タイムゾーンを適用した日付で持つ方が都合が良い事もあるので悩ましいところです。

データを標準時で扱う

データ(java.util.Date)を標準時で扱うのは最も簡単な方法です。

TimeZone timeZone = TimeZoneLocator.get();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(timeZone);
// テキストからDateに変換
Date data = sdf.parse(inputText);
// Dateからフォーマット
String outputText = sdf.format(data);
// Calendarで日付処理を行う
Calendar cal = Calendar.getInstance(timeZone);
cal.setTime(data);
// 処理
Date newData = cal.getTime();

入出力時とCalendarによる日付処理の時にタイムゾーンを意識します。

入力 タイムゾーン適用
データ 標準時
出力 タイムゾーン適用
日付処理 タイムゾーン適用
データをタイムゾーンを適用した文字列として扱う

データを文字列として扱いたい場合にはやや複雑になりますが、基本方針はDateを使わない事です。

TimeZone timeZone = TimeZoneLocator.get();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(timeZone);
SimpleDateFormat sdf2 = new SimpleDateFormat("MM-dd");
sdf2.setTimeZone(timeZone);
// 入力(テキスト)とデータはString
String data = inputText;
// Stringから別のフォーマットに変換
String outputText = sdf2.format(sdf.parse(data));
// Calendarで日付処理を行う
Calendar cal = Calendar.getInstance(timeZone);
cal.setTime(sdf.parse(data));
// 処理
String newData = sdf.format(cal.getTime());

解りにくいので、データを保持するドメインクラスを用意すると良いと思います。

入力 タイムゾーン適用
データ タイムゾーン適用
出力 タイムゾーン適用
日付処理 タイムゾーン適用

追記

コメントで指摘がありました。

slim3を使うのであればorg.slim3.util.DateUtilのtoCalendarやtoDateを使う方がいいと思います。

気付いていませんでした。slim3で提供されているメソッドを使う方が良いですね。ソースも合わせて読んでみると良いと思います。