2012年11月6日火曜日

PHPのタイムゾーン(時差の計算)についての備忘録

今まで全くタイゾーンについて意識していませんでしたが、実際に自分が外国に来てからいざ
Webアプリケーションを作ってみようと思った段階で、「あれ?日付の取り扱いってどうすれば良いんだ?」と疑問に思ったので、この際なので纏めてみました。ちなみにPHPです。

GMTとUTC

基本的に同じ物です。GMTはよくグリニッジ天文台が云々言われるヤツですね。
詳しい違いは調べて頂ければ分かると思います。
基本的にUTCの方が正確な物というイメージですので、PHPプログラム内ではUTCを使用するようにした方が良いと思われます。

時差について

世界中の国々では当然時差があります。
例えば日本が24:00の場合、冬期ならばドイツは前日の16:00となります。
ちなみにドイツにはサマータイムがあるので、夏場だと時差が7時間となります。 この時点でプログラムで態々計算するのが面倒臭そうだというのが想像出来ると思います。

PHPでどのように取り扱うか

PHP5.2からDateTimeという組み込みクラスが使えるようになっています。
いきなり結論ですが、こいつを使えば全ての問題が解決します。
時差の計算などは全てこのクラスに任せればOKなのです。

実際の使い方は以下。

date_default_timezone_set('UTC');
$t = new DateTime("2012-11-05 21:34:40.0000000");

$t->setTimeZone( new DateTimeZone('UTC'));
echo $t->format(DateTime::ISO8601), "(UTC)<br />";

$t->setTimeZone( new DateTimeZone('Asia/Tokyo'));
echo $t->format(DateTime::ISO8601), "(Tokyo)<br />";

$t->setTimeZone( new DateTimeZone('Europe/Berlin'));
echo $t->format(DateTime::ISO8601), "(Berlin)<br />";


実行すると以下のようになります。

2012-11-05T21:34:40+0000(UTC)
2012-11-06T06:34:40+0900(Tokyo)
2012-11-05T22:34:40+0100(Berlin)

簡単なソースですので見れば分かると思いますが、大まかな流れは、
1.タイムゾーンをセット(これはphp.iniでもOK)
2.DateTimeオブジェクトを生成
3.DateTimeオブジェクトのタイムゾーンを変更する
4.formatメソッドを使って時刻を表示する。

今回の場合、タイムゾーンに世界標準時間(UTC)を指定しているので、DateTimeオブジェクトのコンストラクタに渡してあげた時刻文字列は、世界標準時間として扱われます。
以降、生成したDateTImeオブジェクトのタイムゾーンを変更すると、世界標準時間(UTC)からの時差を求める事が出来ます。

じゃあタイムゾーンに東京とか指定してDateTimeオブジェクト生成するとどうなるんだ?
元々日本国内で運営していたサービスを世界展開する場合、データベースなどに保存されている日時は日本時間だぞ?それをUTCとして扱うとおかしくなるんじゃないの?
という疑問が当然出てくると思います。
その点に付いては、下記の「注意点」を参照してください。

*なお、php.iniかスクリプト内でタイムゾーンを設定していないと以下のようなエラーが出ます。
Fatal error: Uncaught exception 'Exception' with message 'DateTime::__construct() [<a href='datetime.--construct'>datetime.--construct</a>]: It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Europe/Berlin' for 'CET/1.0/no DST' instead' in /Users/koji/Sites/flash_cards/hoge.php:3 Stack trace: #0 /Users/koji/Sites/flash_cards/hoge.php(3): DateTime->__construct('2012-11-05 21:3...') #1 {main} thrown in /Users/koji/Sites/flash_cards/hoge.php on line 3


注意点

上記の方法では、既に生成したDateTimeオブジェクトのタイムゾーンを変更する事で、時差を求めています。
言い方を変えると、DateTimeオブジェクトを生成した時のタイムゾーンを元に時差を求めている事になります。

例えば、タイムゾーンを東京としてDateTimeオブジェクトを生成したとします。
で、そのDateTimeオブジェクトからUTC(世界標準時間)を求めた場合、当然のごとくその時間から-9時間となります。

// DateTimeオブジェクトをタイムゾーンをUTCとして生成
date_default_timezone_set('UTC');
$t = new DateTime("2012-11-05 21:34:40.0000000");

$t->setTimeZone( new DateTimeZone('UTC'));
echo $t->format(DateTime::ISO8601), "(UTC)<br />";

$t->setTimeZone( new DateTimeZone('Asia/Tokyo'));
echo $t->format(DateTime::ISO8601), "(Tokyo)<br />";

$t->setTimeZone( new DateTimeZone('Europe/Berlin'));
echo $t->format(DateTime::ISO8601), "(Berlin)<br />";

echo "-----------------------------------------------";

// DateTimeオブジェクトをタイムゾーンをTokyoとして生成
date_default_timezone_set('Asia/Tokyo');
$t = new DateTime("2012-11-05 21:34:40.0000000");

$t->setTimeZone( new DateTimeZone('UTC'));
echo $t->format(DateTime::ISO8601), "(UTC)<br />";

$t->setTimeZone( new DateTimeZone('Asia/Tokyo'));
echo $t->format(DateTime::ISO8601), "(Tokyo)<br />";

$t->setTimeZone( new DateTimeZone('Europe/Berlin'));
echo $t->format(DateTime::ISO8601), "(Berlin)<br />";

実行結果は以下
2012-11-05T21:34:40+0000(UTC)
2012-11-06T06:34:40+0900(Tokyo)
2012-11-05T22:34:40+0100(Berlin)
-----------------------------------------------
2012-11-05T12:34:40+0000(UTC)
2012-11-05T21:34:40+0900(Tokyo)
2012-11-05T13:34:40+0100(Berlin)

同じ時刻でDateTimeを生成しているのに、結果が全く異なっていますね。
冷静に考えればそのままなのですが、あくまでDateTimeオブジェクトを生成した時のタイムゾーンを元に時刻が計算される、という事を覚えておかないとはまってしまいます。
なお、DateTimeオブジェクトを生成する際にコンストラクタに渡している文字列の時刻ですが、これはPostgreSQLのTimestamp型からPDOでSELECTした結果の文字列です。

また、DateTimeに渡す文字列の時刻ですが、UNIXタイムスタンプや、今回のサンプル実行結果のようなタイムゾーン付きの文字列( 2012-11-05T21:34:40+0900 <-こんなやつ)の場合には、コンストラクタに渡すタイムゾーンパラメータや現在のタイムゾーンは無視されてしまいます。

// DateTimeオブジェクトをタイムゾーンをTokyoとして生成
// でも、DateTimeコンストラクタにタイムゾーン付き時刻の文字列を渡すと、その時刻のみを元にDateTimeオブジェクトが生成される
date_default_timezone_set('Asia/Tokyo');
$t = new DateTime("2012-11-01T13:34:40+0100");

$t->setTimeZone( new DateTimeZone('UTC'));
echo $t->format(DateTime::ISO8601), "(UTC)<br />";

$t->setTimeZone( new DateTimeZone('Asia/Tokyo'));
echo $t->format(DateTime::ISO8601), "(Tokyo)<br />";

$t->setTimeZone( new DateTimeZone('Europe/Berlin'));
echo $t->format(DateTime::ISO8601), "(Berlin)<br />";

実行結果
2012-11-01T12:34:40+0000(UTC)
2012-11-01T21:34:40+0900(Tokyo)
2012-11-01T13:34:40+0100(Berlin)


参考URL

http://php.net/manual/ja/class.datetime.php
http://www.php.net/manual/ja/datetime.construct.php
http://www.php.net/manual/ja/class.datetimezone.php
http://www.php.net/manual/ja/datetime.formats.php
http://php.net/manual/ja/function.date-default-timezone-set.php