Edited at

[Groovy|Java]タイムゾーンに関するメモ

More than 3 years have passed since last update.


タイムゾーンとは

世界中の国々の間では当然時差があります。

で、コレをプログラムの中で扱うとなると非常に面倒くさい。ヨーロッパとか夏時間があるし。。。

サービスを世界展開させる場合にもどうしても避けることが出来ません。

タイムゾーンというと難しく聞こえますが、時差の計算と言い換えればわかりやすいかもしれません。


前提条件

サンプルとしてUTC、ドイツ時間(冬時間)、日本時間を扱う。

ドイツ時間はUTCから見ると+1時間、日本時間はUTCから見ると+9時間の時差がある。

ついでに、日本時間はドイツ時間(冬時間)から見ると+8時間の時差があることになる。


いきなり結論

まとめればSimpleDateFomratを使ってTimeZoneをUTCとして、DateはそのSimpleDateFormat経由でいじれば、あとはどうとでもなるってことです!


コード

渡された文字列から、SimpleDateFormatを使ってDateを生成する場合、その時点でSimpleDateFormatが保持するタイムゾーンを元に時差を計算して、その計算結果のDateが生成されます。

なお、Date自体はタイムゾーンを持っていません。Dateは単純にUnixエポックタイム(1970年1月1日00:00:00)から経過したミリ秒を保持しています(getTimeで取得できる)

とうことは、SimpleDateFormatは、タイムゾーンごとに指定された日付文字列からUnixエポックタイム自体を計算して、その値を元にDateを生成していると言い換えることができます。

String dateString = "2016/01/01 00:00:00"

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")

// UTCとしてSimpleDateFormatがDateを生成して、UNIXエポックタイムを取得する
sdf.setTimeZone(TimeZone.getTimeZone("UTC"))
Long epochTimeByUTC = sdf.parse(dateString).time

// ドイツ時間としてSimpleDateFormatがDateを生成して、UNIXエポックタイムを取得する
sdf.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"))
Long epochTimeByGermany = sdf.parse(dateString).time

// 今回の例だとドイツは冬時間なので、ドイツとUTCの時差はドイツの方が+1時間になる。
assert 1451606400000 == epochTimeByUTC
assert 1451602800000 == epochTimeByGermany
assert 3600000 == epochTimeByUTC - epochTimeByGermany
assert 3600000 == 1000 * 60 * 60 // 1時間の時差(1000(ミリ秒) * 60(1分)* 60(60分 == 1時間)

上記の例で、なんでepochTimeByUTCの値のほうが大きいの?という話。

UTCとドイツ時間を比べるとドイツ時間の方が+1時間なんだからepochTimeByGermanyの値のほうが大きくならないとおかし位と思うんだけど。。。

それにはちゃんと理由が合って、epochTimeByGermany2016/01/01 00:00:00をドイツ時間としてSimpleDateFormatからDateを生成しているけど、2016/01/01 00:00:00がドイツ時間の場合、ドイツ時間はUTCから1時間進んでいるため、この時間から-1時間するとUTC時間が分かる。

つまり、UTCは2016/01/01 00:00:00 −1時間なので、2015/12/31 23:00:00になる。

つまり、



  • epochTimeByUTCの方をUTCで表すと2016/01/01 00:00:00


  • epochTimeByGermanyの方をUTCで表すと2015/12/31 23:00:00

がそれぞれ格納されている。

なので、epochTimeByUTCの方がUNIXエポックタイムとして大きな値を持っていることになる。

あくまでUNIXエポックタイムはタイムゾーンの関係ない値になる。

なので、ドイツ時間の2016/01/01 00:00:00と、UTCの2015/12/31 23:00:00は、UNIXエポックタイムとして全く同じになる。

もう一度コードを見てみましょう。

抜粋した以下の場合だと、UTCとして2016/01/01 00:00:00となり、ドイツ時間としては2016/01/01 01:00:00という意味になる。

// UTCとしてSimpleDateFormatがDateを生成して、UNIXエポックタイムを取得する

sdf.setTimeZone(TimeZone.getTimeZone("UTC"))
Long epochTimeByUTC = sdf.parse(dateString).time

抜粋した以下の場合だと、ドイツ時間として2016/01/01 00:00:00となり、UTCとしては2015/12/31 23:00:00という意味になる。

// UTCとしてSimpleDateFormatがDateを生成して、UNIXエポックタイムを取得する

sdf.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"))
Long epochTimeByUTC = sdf.parse(dateString).time


時差を計算して表示する

で、Dateそのままnew Date()で生成した場合はどうなるのでしょう??

コレは実はもっとシンプルです。

すでに述べたように、DateはあくまでUNIXエポックタイムを保持しているわけなので、どんなタイムゾーンで生成したとしても、UNIXエポックタイム的には同じになります。

Dateを生成する際にタイムゾーンを指定することは出来ませんが、コレはUNIXエポックタイムを扱っているわけなので、タイムゾーンはそもそも必要ない、ということを考えると当然のことだと思われます。

結果として、表示する際にSimpleDateFormatのTimeZoneを設定して使えば、問題なく時差を計算した結果の表示が可能です。

なお、以下のコードはドイツ時間で2015年12月8日12時13分に、タイムゾーンがEurope/Berlinに設定されている端末で実行しました。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z ZZ")

Date now = new Date()
sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm z Z")
sdf.setTimeZone(TimeZone.getTimeZone("UTC"))
assert "2015/12/08 11:13 UTC +0000" == sdf.format(now)

sdf.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"))
assert "2015/12/08 12:13 CET +0100" == sdf.format(now)

sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"))
assert "2015/12/08 20:13 JST +0900" == sdf.format(now)

一つのDateオブジェクトから、SimpleDateFormatを使えばちゃんと時差が計算されて表示されていることが解ると思います。

データベースなどの時間を保存する場合、サーバの移動や、複数サーバなどの要因により、デフォルトのタイムゾーンが変わる可能性がある。

そのため、時刻はUTCとして扱い、保存して、表示するときにのみタイムゾーン毎の時差を計算して表示するのが一番シンプルで確実。

Groovy(多分Javaも)問題をややこしくしているのが、単純にDateを出力すると、OSの標準のタイムゾーンを元に、勝手に時差を計算して表示するという点。


サンプルコード


import java.text.SimpleDateFormat

// dateStringをドイツ時間としてDateを生成する。
Date createDateAsGermany(String dateString) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
sdf.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"))
sdf.parse(dateString)
}

// dateStringをUTCとしてDateを生成する。
Date createDateAsUTC(String dateString){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
sdf.setTimeZone(TimeZone.getTimeZone("UTC"))
sdf.parse(dateString)
}

// SimpleDateFormat経由でDateを作成した場合、すでに時刻が正しく計算されてDateオブジェクトが生成される。
// 以下の場合、上記はドイツ時間で作成した日時を、ドイツ時間でフォーマットして出力しようとしているので、当然変化なし。
// ところが2番めの例だと、UTC時間で作成したDateをドイツ時間で表示しようとしている。そのため、ドイツ時間はUTC+1時間なので、1時間進んだ時間が返される。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z Z")
sdf.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"))
assert "2016/01/01 00:00:00 CET +0100" == sdf.format(createDateAsGermany("2016/01/01 00:00:00"))
assert "2016/01/01 01:00:00 CET +0100" == sdf.format(createDateAsUTC("2016/01/01 00:00:00"))

sdf.setTimeZone(TimeZone.getTimeZone("UTC"))
assert "2015/12/31 23:00:00 UTC +0000" == sdf.format(createDateAsGermany("2016/01/01 00:00:00"))
assert "2016/01/01 00:00:00 UTC +0000" == sdf.format(createDateAsUTC("2016/01/01 00:00:00"))