モダンPerlの世界へようこそ

第15回DateTime:APIの標準化をめざして

Cから継承したAPI

プログラマにとって、ログの解析や作成などに含まれる日付や時刻の操作は切っても切り離せない分野のひとつです。もちろんPerlにも日付や時刻を操作するための関数は組み込まれています。

ただし、Cから継承してきたlocaltime()やgmtime()の返り値は、お世辞にもわかりやすいとはいえません。リストコンテキストで呼び出せば年月日、時分秒などの値を取り出せるとはいえ、単一の配列で受け取ると、個々の要素を使うときに直感的ではなくなりますし、明示的な名前をつけたスカラー変数を並べて受け取るのはいかにも冗長です。

use strict;
use warnings;

# 短いけれど非直感的
my @tm = localtime(); $tm[5] += 1900; $tm[4]++;
printf "%04d-%02d-%02d %02d:%02d:%02d\n", reverse(@tm[0..5]);

# わかりやすいけれど冗長
my ($sec, $min, $hour, $day, $mon, $year) = localtime();
$year += 1900; $mon++;
printf "%04d-%02d-%02d %02d:%02d:%02d\n",
    $year, $mon, $day, $hour, $min, $sec;

配列から構造体へ

そのため、Perl 5でオブジェクト指向的な考え方が導入されたのを受けて、1996年11月には、Class::Struct(当時はClass::Templateと呼ばれていました)をベースにしたTime::tmと、それを利用したTime::localtime、Time::gmtimeがそれぞれPerlのコアに導入されました(Perl 5.003_11以降⁠⁠。これを使うと、先の例はもう少し簡潔ないし直感的に書けるようになります。

use strict;
use warnings;
use Time::localtime;
my $tm = localtime();
printf "%04d-%02d-%02d %02d:%02d:%02d",
    $tm->year + 1900, $tm->mon + 1, $tm->mday,
    $tm->hour, $tm->min, $tm->sec;

もっとも、これは配列をオブジェクト化したに過ぎません。$tm->yearは相変わらず1900年からの経過年ですし、$tm->monは0~11の整数です。なまじオブジェクト化した分、演算子を直接適用しづらくなっている面もあり、少なくともPerlの父、ラリー・ウォール氏は満足できなかったようです。

ラリーの仕様から生まれたTime::Piece

氏は、2000年1月のメールで、現行のlocaltime()のインタフェースを廃止できればと述べつつ、ひとつの理想的なインタフェースの案を提示します[1]⁠。

そのメールに反応して生まれたのが(2000年3月のリリース当初はTime::Objectと呼ばれていた)現在のTime::Pieceでした。

Time::Pieceを使うと、先の例はたとえばこのように書き直せます。yearやmonの値にはもう手を加える必要はありません。

use strict;
use warnings;
use Time::Piece;

my $tm = localtime();
printf "%04d-%02d-%02d %02d:%02d:%02d",
    $tm->year, $tm->mon, $tm->mday, $tm->hour, $tm->min, $tm->sec;

また、このようにも書けますし、POSIX的なstrftimeやstrptimeにも対応しています。あまり表示にこだわりがないなら、$tm->datetimeとして(YYYY-MM-DDThh:mm:ssのような)ISO 8601風の出力にしてもよいでしょう。

print $tm->ymd, ' ', $tm->hms, "\n";
print $tm->strftime('%Y-%m-%d %H:%M:%S'), "\n";

また、同梱されているTime::Secondsの助けを借りれば日時の加減算もできます。

use strict;
use warnings;
use Time::Piece;
use Time::Seconds;

my $tm = localtime() + ONE_DAY;
print $tm->datetime, "\n";

YAPCまでの日数を求めたければこのように書けます。

use strict;
use warnings;
use Time::Piece;

my $yapc  = Time::Piece->strptime('2009-09-10', '%Y-%m-%d');
my $delta = $yapc - localtime();
print int($delta->days), "\n";

日時関連モジュールの混沌

このTime::Pieceは、ラリーの仕様を実装したという事情もあり、2001年4月に一度はPerl 5.8系列(正確にはその開発版である5.7系列)のコアに入ります(2001年7月に開発版としてリリースされたPerl 5.7.2を見ると、その様子が確認できます⁠⁠。

ところが、それと相前後するように第二の日時モジュールブームが起こり、2001年の4月から7月にかけて、Date::Handler, Date::ICal, Class::Date, Date::Simpleといったモジュールが立て続けにリリースされたため、5.7.2がリリースされた直後の2001年8月には、Perlの日時モジュールを専門に扱うためにできたばかりのメーリングリストで、⁠最近また日時関連モジュールが乱立しているが、もう少し整理してはどうか」という議論が起こります。

このような議論はDate::CalcDate::ManipDate::Parseといったモジュールが次々にリリースされた1995/1996年の時点ですでにあったのですが、たしかに当時からDate::で始まるモジュールとTime::で始まるモジュールの棲み分けにはあいまいな部分がありました(どちらの名前空間に入っていようと、多くのモジュールは日付も時刻も扱えました⁠⁠。それぞれ独自のAPIを持ち、相互にオブジェクトのやりとりができるような状態でもなかったため、複数の日時関連モジュールを使いたい場合は、いちいちOSのエポック時などに変換しなければならないという制約もありました。そのため、共通のAPIを求める声は少なくなかったのですが、TMTOWTDIをモットーとしてきたPerl界だけに結局調整はつかず、血統的には最も標準に近い位置にあったTime::Pieceでさえ「論争中の名前空間における一実装に過ぎない」という理由でコアから削除されてしまいます[2]⁠。

DateTimeプロジェクト

そのような混乱にいちおうの終止符を打ったのが、デイヴ・ロルスキー(Dave Rolsky)氏が2003年初頭に始めたDateTimeプロジェクトでした。

このプロジェクトについては2006年のYAPC::Asiaでも紹介がありましたし(当時のスライド⁠、さまざまなところで推薦されてきたのですでに使われている方も多いと思いますが、氏は、話し合うだけでは現状の解決にはならないので、既存モジュールの作者を無理に説得するのはやめて、DateTimeというしがらみのない新しい名前空間を計画的に使おうと呼びかけたうえで、前述のTime::Pieceや、不完全ながらもタイムゾーンのサポートを始めていたData::ICalのコードを下地に、やりとりの基本となる(日時を保存しておく)ベースクラスのAPIを固め、その後、既存のコードを大いに参考にしながら、各種のカレンダーや書式、計算といった派生モジュールの対応を行っていく方針を打ち出します。

また、過去や未来のカレンダーにも対応する必要性から、従来データ交換に使われてきたOSのエポック時の制約からの脱却が計られたほか、Time::Pieceと同時期にコア入りしたTime::HiResなどの存在も考慮して、1秒以下の時間への対応なども打ち出されます。

基本的な使い方

とはいえ、ごく表面的なレベルでは、DateTimeの使い方はTime::Pieceなどと大差ありません。

use strict;
use warnings;
use DateTime;

my $dt = DateTime->now;
print $dt->year, "\n";
print $dt->strftime('%Y-%m-%d %H:%M:%S'), "\n";

明示的にタイムゾーンを指定すれば(ふつうは指定します)そのタイムゾーンにあわせた時刻が表示されます。

print DateTime->now(time_zone => 'Asia/Tokyo'), "\n";

特定のエポック時を指定したり、別のDateTimeオブジェクトを利用して初期化することもできます。

my $dt     = DateTime->from_epoch(epoch => time + 3000);
my $new_dt = DateTime->from_object(object => $dt);

後者の例は、単独では価値がわかりにくいですが、DateTime系のモジュールは種類によって特定のAPIを実装することが求められているため、このような汎用的なAPIを覚えておくと、拡張モジュールを利用してほかの表記をしたくなったときに便利です。

use strict;
use warnings;
use DateTime;
use DateTime::Calendar::Japanese;

my $dt = DateTime->new(year => 1876);
my $ja = DateTime::Calendar::Japanese->from_object(object => $dt);
printf "%s (%d-%d)\n",
    $ja->era->id, $ja->era->start->year, $ja->era->end->year;

もっとも、かならずしもすべてがハッシュ渡しではなく、場合によっては文字列やオブジェクト単独で渡す場合もあります。

use strict;
use warnings;
use DateTime;
use DateTime::Format::MySQL;

my $dt = DateTime->now(time_zone => 'Asia/Tokyo');
print DateTime::Format::MySQL->format_datetime($dt);

カレンダーを出力するときに便利なイテレータもあります。

use strict;
use warnings;
use DateTime;
use DateTime::Event::Recurrence;

my $recur = DateTime::Event::Recurrence->daily;

my $dt = DateTime->new(year => 2009, month => 9);
while ($dt->month == 9) {
    print $dt->date, "\n";
    $dt = $recur->next($dt);
}

なお、渡せるキー・値の組み合わせには制約がついている場合があります。ありえない値を渡した場合などはエラーで死にますので、必要に応じてエラーをトラップしなければなりません(詳しくはそれぞれのPODをご確認ください⁠⁠。

# 万一$monthの値が13などになった場合はエラーで死にます
my $dt = eval { DateTime->new(year => $year, month => $month) };
$dt = DateTime->now if $@;

同梱のDateTime::Durationの助けを借りれば加減算を行うこともできます。演算子を使う計算だけでなく、メソッドを使ってより明示的な計算を行うこともできるようになっています。

my $yapc  = DateTime->new(year => 2009, month => 9, day => 10);
my $today = DateTime->today;
my $delta = $yapc - $today;
print $delta->delta_days, "\n";

print DateTime->now->add(days => 5), "\n";

DateTimeプロジェクトに対する反応

長らく待ち望まれてきた共通APIへの反応はおおむね好意的なものでした。2003年だけで19人の作者が50個の関連モジュールをリリースしていますし、既存の日時関連モジュールの中には、2003年のDateTimeリリース以降、更新が止まっているものも少なくありません。DateTimeモジュール群は事実上の業界標準モジュールとして、世界各地で利用されています。

ただし、DateTimeモジュール群にも泣き所がないわけではありません。日時処理は非常駐環境で使われる場合も少なくないだけに、そのロード時間の長さはいささか気になるところですし、モジュール群の大きさやタイムゾーン・データベースの更新頻度などの都合もあってPerlのコアモジュールにはしづらいというのも悩みの種でした。いくらすばらしいモジュールであっても、インストール不要なコアモジュールでなければ使いづらい環境というのはたしかにあります。そのようなギャップを埋める何かが必要でした。

その隙間を埋める試みとしては、まずアダム・ケネディ(Adam Kennedy)氏が2006年の8月末から9月頭にかけてリリースしたDate::TinyTime::Tinyという2つの::Tinyモジュールがあげられます。

これはログファイルの処理など、複雑な計算は必要ない用途向けにコアを軽量化し、必要があれば本家のDateTimeに処理を委譲しようというものだったのですが、氏が提唱した::Tinyというカテゴリーそのものに対する疑問や、それぞれ日付のみ、時刻のみしか扱えないという大きな制約などから、多くの関心を集めるには至りませんでした。

同時期に起こったより重要な出来事としては、一度はコアから外れたTime::Pieceが、2006年11月にふたたびPerl 5.9/5.10系列のコアに入ったことがあげられます。DateTimeに比べれば不完全な部分があるとはいえ、大きさや機能、ロード時間といったコストパフォーマンスを考えればTime::Pieceは十分に現実的な解決策といえます。その仕様が固められた時期や経緯を考慮すると、これもまた未来(=Perl6)からの贈り物とみなしうるのも大きな理由になりました。

また、ごく最近の例としては牧大輔氏によるDateTimeX::Liteの例をあげることもできます。これはまだ開発版の域を出ていませんが、もう少し整備が進めば興味深いものになるかもしれません。

2038年問題

こういった軽量化の話とは別に、2008年にはエポック時から解放されたはずのDateTimeにもまだ2038年問題は残っているという指摘が行われました。この問題については最終的にマイケル・シュワーン(Michael Schwern)氏がPerl Foundationからの助成金を受けて、Perl本体の日時関数の修正を行うことで解決が計られましたが、最新版のPerlを使っていない方は、Time::y2038というモジュールをインストールしておくと、DateTimeのほうでよきに計らってくれるようになります。

どちらを使うにせよ

機能豊富なDateTimeを使うか、軽量なTime::Pieceを使うかは、必要な機能や環境、好みの問題になりますが、少なくとも最初に紹介したような素のlocaltimeやgmtimeを使うやり方は、モダンとはいえません。日時系のモジュールのインストールにはコンパイラが必要になることが多いので、なかなか移行しづらい面もありますが、これは10年ほど前までのPerl/CGIの時代に比べて長足の進歩をとげた分野でもありますので、機会があればぜひモダンな代替品に移行していただければと思います。

おすすめ記事

記事・ニュース一覧