その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Time::SecondsのONE_MONTHとONE_YEARについて

Time::SecondsONE_MONTHONE_YEARを使う場合,本当にその方法で良いのかよく考えたほうが良いと思います.バグが出る可能性が高い気がします.

例えば以下の様な場合

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

my $tp = localtime->strptime("2014-04", "%Y-%m");
say $tp->mon; # => 4
say $tp->ymd; # => 2014-04-01

$tp -= ONE_MONTH;
say $tp->mon; # => 3
say $tp->ymd; # => 2014-03-01

$tp -= ONE_MONTH;
say $tp->mon; # => 1 (!)
say $tp->ymd; # => 2014-01-30 (!!)

このコードは,Time::PieceのオブジェクトからONE_MONTHを引いてやる事によって1ヶ月前の月を表現したいという意図を表していますが,上手く動きません.
それもそのはず,Time::Secondsの実装は以下のようになっており,

https://github.com/rjbs/Time-Piece/blob/master/Seconds.pm#L29-L31

ONE_MONTHは2629744秒 (つまりONE_YEAR / 12) のようにして定義されています.
30日を秒に表すと60 * 60 * 24 * 30 = 2592000秒だからONE_MONTHよりも小さいわけですね.
加えて2014年2月は28日しか無く,つまり60 * 60 * 24 * 28 = 2419200秒なので,ここで決定的に差が生まれてしまったという訳ですね.これは具合が良くない! もちろん「1ヶ月前の今日」みたいなものも上手くは取れないですね.

そしてONE_YEARONE_YEARで31556930秒,つまり365.24225日として表現されている為,こちらも普通に使うとおかしな事になってしまいます.

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

my $tp = localtime->strptime("2014", "%Y");

$tp -= ONE_YEAR;
say $tp->year; # 2012 (!)
say $tp->ymd;  # 2012-12-31 (!!)

のっけからやってくれる!!!! まあそれはそうですよね,という感じ.
こちらも「1年前の今日」みたいなものは上手く取れない.


で,どうするかというと,Time::Piece::Monthを使うと言った方法が考えられますが,このモジュールは内部でDate::Simpleを使っていて,Date::Simpleは呪われている(呪われていた)という問題があるので少し渋い.この方法でも問題なさそうだったらこれで良いとは思います.
問題が複雑では無かったら,LEAP_YEARNON_LEAP_YEARONE_FINANCIAL_MONTHや,あるいはONE_REAL_MONTHONE_REAL_YEARを駆使して自分で組み立てるというのも1つの手かもわかりません.

とにかく,ONE_MONTHONE_YEARを使って何らかの日付け操作をしているコードを見つけた時は疑ってみたほうが良いと思います.


手が空いたらここらへんをケアするシンプルなモジュールを書くのもやぶさかではありませんが,果たして手が空くのか!


[追記]
Time::Piece::Plus使えばええんや!!!!! という僕の中の僕が囁きましたのでシェアします.

[追記]
id:karupanerura氏「add_months/add_years ってメソッド使えや!!!」
とのこと.なんで僕はこのメソッドの存在を知らなかったのだろうか!?

ともかくONE_MONTHとかONE_YEARとかつかってたら大体おかしいことには違いないので,そういう場合は疑うってことでひとつ.