読者です 読者をやめる 読者になる 読者になる

Masteries

技術的なことや仕事に関することを書いていきます.

DateTime::Format::MySQLで作ったDateTimeオブジェクトにタイムゾーンを設定するとエラーになる場合がある

i18n対応したサービスをDateTimeを使ってモリモリ開発している方からすれば既知な話かもしれませんが, 自分は知らなかったのでメモ.

DateTimeとタイムゾーン

DateTimeは, かなりしっかりとタイムゾーンに関する処理が実装されています.

use feature 'say';
use DateTime;

my $dt1 = DateTime->now();
print $dt1->time_zone(); # => UTC

my $dt2 = DateTime->now(
    time_zone => 'Asia/Tokyo',
);
my $dt2->time_zone(); # => Asia/Tokyo

このように, DateTime->new()DateTime->now()において, time_zoneでタイムゾーンを指定すると, 指定したタイムゾーンがセットされたDateTimeオブジェクトを生成することができます(指定しない場合, UTCとして扱われます).

更にDateTimeのオブジェクトは, set_time_zoneメソッドでタイムゾーンを変更することができます.

use feature 'say';
use DateTime;

my $dt = DateTime->now( time_zone => 'Asia/Tokyo' );
say $dt->time_zone->name; # Asia/Tokyo
say $dt->strftime("%F %T"); # 2017-02-13 19:00:00

$dt->set_time_zone("Europe/London");
say $dt->time_zone->name; # Europe/London
say $dt->strftime("%F %T"); # 2017-02-13 10:00:00

サービスの多言語対応などで, ユーザーのタイムゾーンに応じて時間を出し分けたい場合, このset_time_zoneによるタイムゾーンの変更機能は非常に便利です.

DateTime::Format::MySQLについて

さて, DateTime::Format::MySQLを使えば, 次のようにしてMySQLのフォーマットからDateTimeのオブジェクトを生成することができます.

use feature 'say';
use DateTime;
use DateTime::Format::MySQL;

my $dt = DateTime::Format::MySQL->parse_datetime("2017-02-13 19:00:00");
say $dt->time_zone->name; # floating
say $dt->strftime("%F %T"); # 2017-02-13 19:00:00

この時, $dtのタイムゾーンはfloatingになっています. DateTime::Format::MySQLのドキュメントを見ると, 次のように書かれています:

This class offers the following methods. All of the parsing methods set
the returned DateTime object’s time zone to the floating time zone,
because MySQL does not provide time zone information.

要するに, MySQLではタイムゾーンに関する情報を提供していないので, floatingというタイムゾーンとしてDateTimeオブジェクトを作っている, という訳です.

use DateTime;
use DateTime::Format::MySQL;

my $dt = DateTime::Format::MySQL->parse_datetime("2017-02-13 19:00:00");
$dt->set_time_zone("Asia/Tokyo");
say $dt->time_zone->name; # Asia/Tokyo
say $dt->strftime("%F %T"); # 2017-02-13 19:00:00

このように, DateTime::Format::MySQLで生成したDateTimeオブジェクトに, set_time_zoneメソッドでタイムゾーンを指定すれば, 任意のタイムゾーンのDateTimeオブジェクトが取得できます. 上記の例の場合, Asia/Tokyoタイムゾーンにおける2017-02-13 19:00:00のDateTimeオブジェクトが取得できるわけです.

DateTime::Format::MySQLで作ったDateTimeオブジェクトにタイムゾーンを設定するとエラーになる場合がある

…いよいよ本題です. 論より証拠ということで, 実際のコードを見てみましょう.

use feature 'say';
use DateTime;
use DateTime::Format::MySQL;

my $dt = DateTime::Format::MySQL->parse_datetime("2017-03-26 01:00:00");
$dt->set_time_zone("Europe/London");
say $dt->time_zone->name;
say $dt->strftime("%F %T");

このコードを実行すると, 次のようなエラーになります.

Invalid local time for date in time zone: Europe/London

サマータイムの罠

原因は, 英国夏時間です. Wikipediaによると, 英国夏時間は次のような仕組みになっているそうです:

具体的な実施期間は、3月最終日曜日1時(UTC)から10月最終日曜日1時(UTC)までの期間。つまり、グリニッジ平均時で3月最終日曜日の1時になった瞬間、英国夏時間が始まり、同日の2時になる。また、英国夏時間で10月最終日曜日の2時になった瞬間、グリニッジ平均時に戻り、同日の1時になる。このため、夏時間の開始日は1日が23時間となり、終了日は1日が25時間となる。

要するに英国夏時間において, 3月26日午前1時0分〜3月26日午前1時59分は存在しないわけです.

そのため, 上記のように, floatingタイムゾーンにおける2017-03-26 01:00:00のDateTimeオブジェクトに対してset_time_zoneEurope/Londonを指定すると, DateTimeオブジェクトはサマータイムによって存在するはずのないEurope/Londonタイムゾーンにおける2017-03-26 01:00:00になるようにタイムゾーンを書き換えようとします. そのために, 先程のようなエラーが表示されるのでした.

まとめ

これまで割とサマータイムもなく, 特段タイムゾーンを意識しなくてよいJSTの世界に閉じこもって(?)コードを書いていたので, 今回のタイムゾーンやサマータイムの話は調べることがいろいろあって学びがありました.

タイムゾーンやサマータイムの扱い, 非常に難しいものがありますが, 引き続き頑張っていきたいものです.