Subscribed unsubscribe Subscribe Subscribe

Time::Pieceでadd_monthsするときは月末の扱いに気をつける

Perl

現在のTime::Pieceの最新版は、1.27。
翌月は何月か、というのを得るのに

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;

my $t = localtime;
print $t->add_months(1)->mon, "\n";

というコードを書いていて、1月31日に実行したら3が返ってきていてハマった。という話。

Time::Piece::add_monthsで月を操作した結果の日付がその月の月末を超えていると、正規化されて翌月の日付になってしまうようだ。

$ perl -MTime::Piece -E 'say Time::Piece->localtime->ymd'
2014-01-31
$ perl -MTime::Piece -E 'say Time::Piece->localtime->add_months(1)->ymd'
2014-03-03

ので、「翌月は何月か」を正確にとりたい場合は安易にadd_months(1)を使わずに、別の方法で取得する必要がある。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds;

my $now = Time::Piece->localtime;
my $last_day = Time::Piece->localtime->strptime(
    sprintf('%04d-%02d-%02d', $now->year, $now->mon, $now->month_last_day),
    '%Y-%m-%d',
);
my $next_month = ($last_day + ONE_DAY)->mon;
print $next_month, "\n";

こんなかんじで、「現在の月の最終日」をmonth_last_dayで取得できるので、それを使って月末の日付をstrptimeから生成、その1日後の日付から月数を取得する。
非常に面倒だけど。

DateTimeの場合は

DateTime(現在の最新は1.06)はそのへん考慮して計算できるようになっていて、普通にaddすると同じような挙動だが、end_of_monthを指定することでその扱いを変えることができる。

$ perl -MDateTime -E 'say DateTime->now->ymd'
2014-01-31
$ perl -MDateTime -E 'say DateTime->now->add(months => 1)->ymd'
2014-03-03
$ perl -MDateTime -E 'say DateTime->now->add(months => 1, end_of_month => "preserve")->ymd'
2014-02-28

end_of_monthは"wrap", "limit", "preserve"の3種類を指定可能で、加算の場合は"wrap"が、減算の場合は"preserve"がデフォルトになっているらしい。
"wrap"はTime::Pieceの挙動と同じで、月末を超えていた場合は翌月の日付にする。が、"limit", "preserve"の場合は月が変わることなく月末の日付になる。
"limit"と"preserve"の違いは元々の日付が月末だったときに加減算後にも合わせて月末にするか否か、の挙動らしい。例えば翌月の方が長い場合。

$ perl -MDateTime -E 'say DateTime->new(year => 2014, month => 2, day => 28)->add(months => 1, end_of_month => "limit")->ymd'
2014-03-28
$ perl -MDateTime -E 'say DateTime->new(year => 2014, month => 2, day => 28)->add(months => 1, end_of_month => "preserve")->ymd'
2014-03-31

"preserve"だと、月末に加算した結果も月末になっている。

まとめ

…というようなことが全部podにちゃんと書いてあるから、よく読もう。

ちなみに

Rubyの場合は

$ ruby -rDate -e 'puts Date.new(2014, 1, 31)'
2014-01-31
$ ruby -rDate -e 'puts Date.new(2014, 1, 31) >> 1'
2014-02-28

簡単でいいですね…