Perl Hackers Hub

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

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはコルクことkolukuさんで,テーマは「Perlの時間モジュール」です。

業務でコードを書くうえで時間は重要な要素です。日付の管理や特定の形式への変換,ミリ秒まで取り扱いたい場合など,さまざまなケースがあります。Perlには,これらの時間の取り扱いを便利にするモジュールがあります。本稿では,Perlの時間モジュールについて,標準モジュールと拡張モジュールの両方の側面から追っていきます。

本稿は,執筆時点2021年1月の最新版であるPerl 5.32.0を用いました。本稿のサンプルコードは,WEB+DB PRESS Vol.121のサポートサイトから入手できます。

Perlにおける時間の基本

本節では,Perlが時間をどのように管理しているのか,そして時間をどのように扱うのかを解説します。

UNIX時間による時間管理

まずは,Perlが時間をどのように管理しているのかを見ていきます。

Perlではtime関数で現在時間を取得できます。

say time; # 1609426800

このときに得られる時間はUNIX時間です。UNIX時間とは,UTCCoordinated Universal Time協定世界時)での1970年1月1日00:00:00を紀元に,閏秒うるうびょうの存在を無視した紀元からの経過時間を秒数で示したものです。閏秒とは,地球の自転から決まる世界時と,原子時計によって決まる原子時の時刻の差が0.9秒を超えないように,協定世界時に追加や削除される秒のことです。

UNIX時間は,組込み変数の$^Tでも取得できます。

say $^T; # 1609426800
2100年以降での不具合の可能性

UNIX時間は,例外的にClassic Mac OSなど一部の古いシステムでは,UTC1904年1月1日00:00:00が紀元になることがあります。1904年を紀元としている理由は,閏年うるうどしの例外条件を可能な限り回避できる最も古い年だからです。

閏年は,平年より1日多い366日です。閏年は,次の❶から強い優先度で定義されています。

  • ❶ 西暦が400で割り切れる年は閏年
  • ❷ 西暦が100で割り切れる年は平年
  • ❸ 西暦が4で割り切れる年は閏年

そのため,1900年は100年に一度の例外で4で割り切れる年にもかかわらず閏年が発生せず,2000年は400年に一度の例外で100で割り切れる年にもかかわらず閏年が発生します。そして,次の閏年が発生しない年は,100で割り切れ400で割り切れない2100年です。よって,1904年からUNIX時間を始めると,2096年までは4年に一度必ず閏年が存在します。そのため,2100年までは閏年が発生しない場合の例外処理が不要になるという利点があります。

ですが,裏を返せば2100年以降で例外処理を行わないままだと日付などの表示でズレが生じ,それ以降はソフトウェアが正常に動かなくなる割り切った設計でもあります。

2038年問題による不具合の可能性

time関数で取得しているUNIX時間は,標準Cライブラリのtime_t型から取得しています。time_t型は,現在普及しているシステムでは符号付き64ビットintで実装されていますが,古いシステムでは符号付き32ビットintで実装されていることがあります。

これにより,UNIX時間の2の31乗-1秒,つまりUTC2038年1月19日03:14:07を超えると値がオーバーフローして,UTC1901年12月13日20:45:52まで時間が戻り,時刻を正しく扱えなくなります。

時刻の表現

ここまで,PerlがUNIX時間を基準にして時間を管理していることがわかりました。次は,Perlが時間をどのように扱うのかを見ていきます。

time関数はUNIX時間で返されるため,このままでは西暦何年の何時何分何秒なのかを把握できません。そのため,UNIX時間を扱いやすい形で表現する必要があります。localtime関数を用いると,現在時間をさまざまな形で取り出せます。

localtime関数は,time関数が返す時刻を,マシンのタイムゾーンの時刻として秒,分,時,日,月,年,曜日,その年の何日目,夏時間かどうかの9要素の配列に変換します。インデックス番号4の月は0から11までの1ずれた月を持ち,インデックス番号5の年は1900年からの年数を持ちます。

my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
$isdst) = localtime;
say $year + 1900; # 2021

localtime関数をスカラコンテキストのときに扱うと,日付情報を含めて文字列で出力されます。

say scalar localtime; # Fri Jan 1 00:00:00 2021

また,localtime関数は引数にUNIX時間を与えることで,その時間の値を返します。このとき渡したUNIX時間はtime関数の値を上書きせず,あとに続くlocaltime関数はもとの時間を返します。

# Fri Jan 1 00:00:00 2021
say scalar localtime(1609426800);
タイムゾーンの変換

前述したようにlocaltime関数は自身のマシンのタイムゾーンの時刻になるため,日本ではUTCに+09:00を足した時刻になります。

ほかのタイムゾーンの時刻に変換するには,自身のマシンの時刻が日本のものだとわかっているのであれば,単純に9時間を減算してUTCに戻したあとに求めるタイムゾーンの時間を加算することで行えます。しかし,海外のサーバなど自身のマシンのタイムゾーンが不明な場合は,UTCに戻すことができません。

そこで,最初からUTCで出力されるgmtime関数があります。この関数は,localtime関数と同じAPIで扱えます。gmtime関数をリストコンテキストのときに扱うと,夏時間は無効になります。

# 2021-08-01 12:00:00に実行
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
$isdst) = gmtime;
say $isdst; # 常に0
指定した秒数の実行停止

指定した秒数の間実行を停止するには,sleep関数を使います。

say scalar localtime; # Fri Jan 1 00:00:00 2021
sleep 10; # 10秒間,プログラムを実行停止
say scalar localtime; # Fri Jan 1 00:00:10 2021

著者プロフィール

koluku

北海道生まれ。

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

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