こんにちは、アプリケーションエンジニアの id:nanto_vi です。この記事ははてなデベロッパーアドンベントカレンダー 2015 の 2 日目です。
Perl で日時を扱うモジュールのひとつに Time::Piece
があります。コアモジュールなので手軽に使えますが、Perl - Time::Piece に関するとりとめのないコト - Qiita にもまとめられているように注意すべき点も多いです。ここでは、そのような注意点をいくつか挙げていきたいと思います。
なお、以下のコードは Time::Piece
1.31 で確認しています。
タイムゾーン情報を持たない
大前提として、Time::Piece
のインスタンスはタイムゾーン情報を持っていません。持っているのはローカル時間か GMT かの区別のみです。その「ローカル時間」が実際にどのタイムゾーンなのかは、環境 (環境変数 TZ
の値など) から判別して都度計算しています。
インスタンスの複製
Time::Piece
のインスタンスを複製するには、new
メソッドをインスタンスメソッドとして使います。
use Time::Piece; my $t0 = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); my $t1 = $t0->new; say $t0->datetime; # 2015-01-01T00:00:00 say $t1->datetime; # 2015-01-01T00:00:00 say $t0->tzoffset; # 0 say $t1->tzoffset; # 0
new
をクラスメソッドとして使ってしまうと、元のインスタンスが GMT であっても、ローカル時間のインスタンスが生成されます。
use Time::Piece; my $t0 = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); my $t1 = Time::Piece->new($t0); say $t0->datetime; # 2015-01-01T00:00:00 say $t1->datetime; # 2015-01-01T00:00:00 say $t0->tzoffset; # 0 say $t1->tzoffset; # 32400
ローカル時間と GMT の変換
gmtime
/ localtime
関数の第 1 引数に Time::Piece
のインスタンスを渡すと、そのインスタンスの日時の成分 (年月日時分秒の値) を保ったままの、新たな Time::Piece
のインスタンスが生成されます (Time::Piece
1.16 以降)。
エポック秒 (協定世界時 1970 年 1 月 1 日 0 時 0 分 0 秒からの経過秒数) を保ったままで新たなインスタンスを生成したいときは、gmtime
/ localtime
の第 1 引数にエポック秒を渡す必要があります。
use Time::Piece; my $t0 = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); my $t1 = $t0->new; my $t2 = $t0->new; say localtime($t1)->datetime; # 2015-01-01T00:00:00 say localtime($t2->epoch)->datetime; # 2015-01-01T09:00:00
gmtime
と Time::Piece::gmtime
は異なる
多くの Perl モジュールでは、インポートした関数とパッケージ名付きで呼び出す関数は同一です。例えば、use List::Util qw(sum); sum 1 .. 10;
としようが、use List::Util; List::Util::sum 1 .. 10
としようが、挙動に変わりありません。
しかし、Time::Piece
からインポートしてきた gmtime
/ localtime
関数は Time::Piece::gmtime
/ Time::Piece::localtime
関数と挙動が異なります。
インポートしてきた関数は Time::Piece->gmtime
/ Time::Piece->localtime
相当なので、第 1 引数に Time::Piece
のインスタンスを渡したときにそれが結果に影響します。Time::Piece::gmtime
/ Time::Piece::localtime
は第 1 引数に Time::Piece
のインスタンスを渡しても無視します。
use Time::Piece; my $t0 = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); my $t1 = $t0->new; my $t2 = $t0->new; say localtime($t1)->datetime; # 2015-01-01T00:00:00 say Time::Piece::localtime($t2)->datetime; # 2015-12-02T12:34:56 (実行時の時刻)
ローカル時間での日時のパース
Time::Piece->strptime
の返り値は常に GMT になります。strptime
をインスタンスメソッドとして呼び出すと、元のインスタンスのローカル時間フラグが、新しく生成されるインスタンスに引き継がれます。
use Time::Piece; my $g = gmtime; say $g->strptime('2015-01-01', '%Y-%m-%d')->tzoffset; # 0 my $l = localtime; say $l->strptime('2015-01-01', '%Y-%m-%d')->tzoffset; # 32400
strftime
はバイト列を返す
strftime
メソッドの引数に文字列を渡しても、その返り値はバイト列になります。また、引数に NUL 文字が含まれていた場合、それ以降は無視されます。
use utf8; use Time::Piece; use Encode; my $t = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); my $b = $t->strftime("%Y年%m月%d日\0%H自%M分%S秒"); # バイト列なので Wide character in ... 警告が出ない say $b; # 2015年01月01日 # UTF-8 で符号化されたバイト列なので 17 バイトになる say length $b; # 17 # decode_utf8 で文字列に変換すると 11 文字になる。 say length decode_utf8($b); # 11
gmtime
/ localtime
は破壊的
gmtime
/ localtime
関数の第 1 引数に Time::Piece
のインスタンスを渡すと、そのインスタンスの内部的なエポック秒の値が変更されます。どう変更されるかは Time::Piece
1.29 以前と 1.30 以降で異なりますが、1.30 以降だと値の不整合が発生することもあります。
use Time::Piece 1.31; my $t0 = Time::Piece->strptime('2015-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'); say $t0->epoch; # 1420070400 my $t1 = localtime($t0); say $t0->datetime; # 2015-01-01T00:00:00 say $t0->tzoffset; # 0 # 2015-01-01-T00:00:00+00:00 のエポック秒は 1420070400 のはずだが、 # localtime 関数の引数に渡した後はエポック秒だけが # 2015-01-01T00:00:00+09:00 のものになっている。 say $t0->epoch; # 1420038000
まとめ
このように、Time::Piece
には多くの注意点があるため、利用する際には慎重に扱う必要があります。
また、国際化を前提とする (複数のタイムゾーンを扱う必要がある) ときはそもそも Time::Piece
は向いていないので、DateTime
などタイムゾーン操作に対応したモジュールを使ったほうがいいでしょう。
はてなでは日時を正確に扱えるデベロッパーを募集しています (※ 自身が日時に正確である必要はありません)。