Perl Hackers Hub

第66回 モジュールによる時間の多様な取り扱い(2)

この記事を読むのに必要な時間:およそ 4 分

前回の(1)こちらから。

日付表現を便利にするモジュール

(1)では時刻に関わるモジュールを見てきました。しかし,長い未来を見ていく場合,日付のほうが重要です。たとえば2つの異なるタイムゾーンでの日付の比較は,タイムゾーンの差の計算と時間の計算,そしてそれに伴う日付の移動の発生が起こり得ます。これらの計算は,日付計算に優れたモジュールを使用すると便利になります。

(2)では,日付処理に関わるモジュールを見ていきます。

DateTime ─⁠─日付演算を正確に行うモジュール

日付処理の代表的なモジュールとして,DateTime(執筆時点のバージョンは1.54)があります。DateTimeオブジェクトは,newfrom_epochメソッドなどで時間を直接指定して作成するか,nowメソッドやtodayメソッドで実行日時から作ることができます。

use DateTime;
my $dt = DateTime->new(
    year => 2021,
    month => 1,
    day => 1,
    hour => 0,
    minute => 0,
    second => 0,
    # nanosecond => 0,
    time_zone => 'Asia/Tokyo'
);
my $now = DateTime->now(time_zone => 'Asia/Tokyo');

DateTimeオブジェクトは加算や減算の処理が行えますが,オブジェクトに破壊的な変更を加えるため,もとの情報は失われます。演算前の情報を残しておきたい場合は,Cloneモジュール(執筆時点のバージョンは0.45)などであらかじめオブジェクトを複製する必要があります。

use DateTime;
use Clone qw(clone);
my $dt_1 = DateTime->now(time_zone => 'Asia/Tokyo');
my $dt_2 = clone($dt_1);
$dt_2->add(months => 1, days => 1);

演算結果が月末を超える場合の最後の日の合わせ方は,wraplimitpreserveのいずれかをend_of_monthに指定できます。wrapは月末を超えて演算します。limitは月末を超えたら月末に合わせます。preserveは月末を超えたら月末に合わせたうえで,もとの日付が月末の場合は超えた月の月末に合わせます。値が正の場合の初期値はwrapで,負の場合の初期値はpreserveです。

$dt->add(months => 1, end_of_month => 'preserve');

また,DateTimeオブジェクトどうしの減算を行う場合,基点にしたいDateTimeオブジェクトのdelta_daysメソッドに,引きたいDateTimeオブジェクトを渡すことで,DateTime::Duration(執筆時点のバージョンは1.54)オブジェクトが作られます。差の結果を取り出すには,DateTimeオブジェクトのin_unitsメソッドで取り出したい単位を指定します。

use DateTime;
my $dt_1 = DateTime->new( year => 2021, month => 1, day
=> 1, time_zone => 'Asia/Tokyo' );
my $dt_2 = DateTime->new( year => 2021, month => 2, day
=> 1, time_zone => 'Asia/Tokyo' );
my $duration = $dt_2->delta_days($dt_1);
say $duration->in_units('days'); # 差は31日

異なるタイムゾーンのDateTimeオブジェクトどうしの場合は,set_time_zoneメソッドで一度UTCに変換してから演算を行い,もとのタイムゾーンに戻します。

$dt_1->set_time_zone('UTC');

DateTimeモジュールは,タイムゾーンを指定しなかった場合,フローティングタイムゾーンとして扱い,閏秒を考慮しなくなります。指定した場合はそのタイムゾーンの閏秒を扱えます。

しかし,閏秒がいつ挿入されるかは半年前にならないとわかりません。DateTimeモジュールは,閏秒の挿入が決定されるとモジュールのバージョンアップで対応しているため,閏秒に対応していくためにはそのたびにモジュールを更新する必要があります。

閏秒を考慮しない場合は,Time::Pieceモジュールや次項のTime::Momentモジュールなどの比較的軽量なモジュールを検討するとよいです。

Time::Moment ─⁠─高速な日付演算モジュール

Time::Moment(執筆時点のバージョンは0.44)は,日付演算の軽量モジュールです。DateTimeモジュールと似た感覚で扱え,タイムゾーンの代わりにUTCからの時間の差をオフセットとして分で指定することで表現します。

use Time::Moment;
my $tm = Time::Moment->new(
    year       => 2021,
    month      => 1,
    day        => 1,
    hour       => 0,
    minute     => 0,
    second     => 0,
    # nanosecond => 0,
    offset     => 60 * 9, # 日本時間
);
my $now = Time::Moment->now;
my $utc = Time::Moment->now_utc;
my $epoch = Time::Moment->from_epoch(1000);

Time::Momentオブジェクトは,DateTimeオブジェクトの破壊的な加算や減算とは異なり,演算後に新たにTime::Momentオブジェクトが作られる非破壊的な処理を行います。また,月を加算するplus_monthsや減算するminus_monthsは,その月に日が存在しないならその月の最終日になり,存在する場合は日付が維持されます。

$tm2 = $tm1->plus_months(1); # 1ヶ月を加算

DateTimeモジュールは実行速度がかなり遅いです。Benchmarkモジュール(執筆時点のバージョンは1.23)を使って各モジュールの現在時刻を取得するコードの実行速度を筆者のマシンで計測すると,DateTimeモジュールよりTime::Pieceモジュールが約25倍速く,Time::MomentモジュールにいたってはDateTimeモジュールより約140倍速いです。頻繁に日付処理を行う場合は,DateTimeモジュールからTime::Momentモジュールへの移行を検討してもよいでしょう。

use Benchmark qw(cmpthese);
use Time::Moment;
use Time::Piece;
use DateTime;
cmpthese 0, {
    'DateTime'     => sub {
        DateTime->now(time_zone => 'Asia/Tokyo')
    },
    'Time::Piece'  => sub { Time::Piece->localtime },
    'Time::Moment' => sub { Time::Moment->now },
};

著者プロフィール

koluku

北海道生まれ。

2020年に面白法人カヤックに入社し,現在はサーバーサイドアプリケーションを開発している。Webとゲームが好きなひよっこエンジニア。

Twitter:@koluku
URL:https://koluku.com