omae です。
今回は Python でタイムゾーンが扱いたくなり Trac 0.11 より利用されている pytz を使ってみました。
pytz はタイムゾーンのデータベースを中に取り込んでおり、プラットホームに依存せずにタイムゾーンを扱えるようになっています。同種のライブラリとして Ruby では TZInfo などがあるようです。
大抵の Linux ディストリビューションでは /usr/share/zoneinfo にタイムゾーンのデータベース(tzdata)がインストールされているので二重にデータベースを持っているのが個人的には少し気持ち悪い感じがします。(連携できないのかな?)
http://pypi.python.org/pypi/pytz には egg ファイルが用意されているので、すぐにインストールできるようになっています。ということでさくっとインストールしてみてください。今回使用したのは pytz-2009j です。それでは使ってみます。
まずは、日本のタイムゾーンを取得します。これには pytz.timezone を用います。
>>> from datetime import datetime, tzinfo
>>> import pytz
>>> tz_tokyo = pytz.timezone('Asia/Tokyo')
>>> tz_tokyo
<DstTzInfo 'Asia/Tokyo' CJT+9:00:00 STD>
>>> isinstance(tz_tokyo, tzinfo)
True
ここで返ってくるインスタンスは datetime.tzinfo のインスタンスになります。
次に UTC の現時刻を取得しますが、datetime.utcnow() は使わずに datetime.now(), pytz.utc を使ってタイムゾーンが指定された datetime インスタンスを取得しておきます。
>>> pytz.utc
<UTC>
>>> isinstance(pytz.utc, tzinfo)
True
>>> utcnow = datetime.now(pytz.utc)
>>> utcnow
datetime.datetime(2009, 8, 5, 14, 10, 10, 744246, tzinfo=<UTC>)
>>> format = '%Y-%m-%d %H:%M:%S %Z%z'
>>> utcnow.strftime(format)
'2009-08-05 14:11:26 UTC+0000'
datetime.utcnow() を使用しない理由ですが、tzinfo クラスのメソッドのいくつかは datetime インスタンスにタイムゾーンが指定されていないと動作しないものがあるためです。たとえば pytz.utc.fromutc() に datetime.utcnow() を渡すとエラーになってしまいます。
>>> pytz.utc.fromutc(datetime.utcnow())
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: fromutc: dt.tzinfo is not self
>>> pytz.utc.fromutc(utcnow)
datetime.datetime(2009, 8, 5, 14, 11, 26, 183394, tzinfo=<UTC>)
さて、tz_tokyo と utcnow を使って日本時間を表示してみます。
>>> tz_tokyo.fromutc(utcnow)
datetime.datetime(2009, 8, 5, 23, 11, 26, 183394, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
>>> tz_tokyo.fromutc(utcnow).strftime(format)
'2009-08-05 23:11:26 JST+0900'
ちゃんと +9 時間になっています。ついでにスウェーデンの現地時刻も表示してみます。
>>> tz_sweden = pytz.timezone('Europe/Stockholm')
>>> tz_sweden
<DstTzInfo 'Europe/Stockholm' CET+1:00:00 STD>
>>> tz_sweden.fromutc(utcnow).strftime(format)
'2009-08-05 15:37:07 CEST+0200'
日本時間から見て -7 時間のようです。
次は pytz からタイムゾーンの一覧ですが、これには pytz.common_timezones を用います。pytz.all_timezones というのもありますが、こちらにはすでに廃止されたタイムゾーンなども含まれているので通常使う分には common_timezones で問題ないと思います。
>>> len(pytz.common_timezones)
393
>>> pytz.common_timezones[:10]
['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmara', 'Africa/Bamako', 'Africa/Bangui', 'Africa/Banjul', 'Africa/Bissau', 'Africa/Blantyre']
>>> pytz.common_timezones[-10:]
['Pacific/Wake', 'Pacific/Wallis', 'US/Alaska', 'US/Arizona', 'US/Central', 'US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']
このようにタイムゾーン名のリストになっています。実際には利用者に 300 を超えるタイムゾーンの中から選択してもらうのは大変ですので、タイムゾーンごとに GMT からのオフセットを取得することを考えてみます。
tzinfo のリファレンスを参照してみると utcoffset() というメソッドがありオフセットを取得するよいように見えるのですが、pytz が返してくる tzinfo インスタンスに対して utcoffset() を使用すると間違った値を返してくるタイムゾーンが出てきます。
>>> tz_tokyo.utcoffset(utcnow)
datetime.timedelta(0, 32400) # GMT+09:00
>>> tz_sweden.utcoffset(utcnow)
datetime.timedelta(0, 3600) # GMT+02:00
>>> tz_fiji = pytz.timezone('Pacific/Fiji')
>>> tz_fiji.utcoffset(utcnow)
datetime.timedelta(0, 42840) # GMT+11:54?
フィジーのタイムゾーンは http://ja.wikipedia.org/wiki/フィジー にある通り GMT+12:00 (43200) ですが GMT+11:54 (42840) という中途半端な値を返してきてしまいます。
この症状に似たのはないかとバグデータベースのほうを探してみると当てはまるものがあり workaround も示されていました。Bug #310606 in pytz: “Incorrect utcoffset returned from timezones” です。これに従いやり直してみると
>>> tmp = datetime.now()
>>> pytz.utc.localize(tmp) - tz_fiji.localize(tmp)
datetime.timedelta(0, 43200) # GMT+12:00
となります。この式を用いて (GMT+12:00) Pacific/Fiji
のような文字列に変換しオフセットでソートすれば少しは選択しやすくなると思います。
さらに選択しやすくするには
- 国(ISO-3166)を選択してもらい pytz.country_timezones から該当するタイムゾーンだけを表示する (Google Calendar ではこのようになっています)
- javascript を用いて
new Date().getTimezoneOffset()
に該当するタイムゾーンだけを表示する
などの方法があるかと思います。
日本にいると夏時間がなく使用するタイムゾーンは1つしかないので、タイムゾーンを意識してコードを書くことはほとんどありませんが、pytz を知っておけば今後どこかで役に立つことがあるかも知れません。