本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはコルクことkolukuさんで、テーマは「Perlの時間モジュール」です。
業務でコードを書くうえで時間は重要な要素です。日付の管理や特定の形式への変換、ミリ秒まで取り扱いたい場合など、さまざまなケースがあります。Perlには、これらの時間の取り扱いを便利にするモジュールがあります。本稿では、Perlの時間モジュールについて、標準モジュールと拡張モジュールの両方の側面から追っていきます。
本稿は、執筆時点2021年1月の最新版であるPerl 5.32.0を用いました。本稿のサンプルコードは、WEB+DB PRESS Vol.121のサポートサイトから入手できます。
Perlにおける時間の基本
本節では、Perlが時間をどのように管理しているのか、そして時間をどのように扱うのかを解説します。
UNIX時間による時間管理
まずは、Perlが時間をどのように管理しているのかを見ていきます。
Perlではtime
関数で現在時間を取得できます。
このときに得られる時間はUNIX時間です。UNIX時間とは、UTC(Coordinated Universal Time、協定世界時)での1970年1月1日00:00:00を紀元に、閏秒の存在を無視した紀元からの経過時間を秒数で示したものです。閏秒とは、地球の自転から決まる世界時と、原子時計によって決まる原子時の時刻の差が0.9秒を超えないように、協定世界時に追加や削除される秒のことです。
UNIX時間は、組込み変数の$^T
でも取得できます。
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年からの年数を持ちます。
localtime
関数をスカラコンテキストのときに扱うと、日付情報を含めて文字列で出力されます。
また、localtime
関数は引数にUNIX時間を与えることで、その時間の値を返します。このとき渡したUNIX時間はtime
関数の値を上書きせず、あとに続くlocaltime
関数はもとの時間を返します。
タイムゾーンの変換
前述したようにlocaltime
関数は自身のマシンのタイムゾーンの時刻になるため、日本ではUTCに+09:00を足した時刻になります。
ほかのタイムゾーンの時刻に変換するには、自身のマシンの時刻が日本のものだとわかっているのであれば、単純に9時間を減算してUTCに戻したあとに求めるタイムゾーンの時間を加算することで行えます。しかし、海外のサーバなど自身のマシンのタイムゾーンが不明な場合は、UTCに戻すことができません。
そこで、最初からUTCで出力されるgmtime
関数があります。この関数は、localtime
関数と同じAPIで扱えます。gmtime
関数をリストコンテキストのときに扱うと、夏時間は無効になります。
指定した秒数の実行停止
指定した秒数の間実行を停止するには、sleep
関数を使います。
時間の標準モジュール
localtime
関数やgmtime
関数で時間をある程度は自由に表現できるものの、時間の加算や減算などの演算が煩雑であることや、ミリ秒の取り扱いができないなどの問題があります。
本節では、時間演算を便利に行う標準モジュールと、ミリ秒まで時間を扱う標準モジュールを見ていきます。
Time::Piece ──高速な時間演算モジュール
localtime
関数は時間を任意の単位で取り出せるため加工するのに便利ですが、使いたい形にするためには関数化する必要があります。日付や時刻情報はISO8601などで出力フォーマットが決められていることが多く、その形に毎回変換するのは煩雑です。
Time::Piece
モジュール(執筆時点のバージョンは1.3401)は、localtime
関数で得られていた分や日など単体の時間はもちろん、日付フォーマットの設定を行うメソッドを持っています。localtime
関数とgmtime
関数をオーバーライドして、Time::Piece
オブジェクトとして結果が返却されます。
Time::Piece
モジュールではUNIX時間から任意の時間を得るのではなく、文字列からTime::Piece
オブジェクトに変換できます。
localtime
関数では、時間の加算や減算はUNIX時間を直接渡して出力を得ていました。
それに対してTime::Piece
モジュールでは、Time::Piece
オブジェクトに対してTime::Seconds
オブジェクトを加算や減算できます。このとき得られるのはTime::Piece
オブジェクトです。
また、Time::Piece
オブジェクトどうしを減算することで時間の差を得られます。このとき得られるのはTime::Seconds
オブジェクトです。
さらに、Time::Piece
オブジェクトどうしで比較できます。
Time::Secondsモジュールの定数の罠
Time::Seconds
モジュールでは、1年の定数と1ヵ月の定数は次の内容で定義されています。
- ONE_YEAR:365.24225日
- ONE_MONTH:ONE_YEAR / 12
ONE_YEAR
は365日と定義されていません。これは、閏年を考慮して400年の平均の値となっているためです。その影響を受けて、ONE_MONTH
は30日よりも10時間ほど多い値になっています。
閏年を考慮せず常に30日として扱いたい場合はONE_FINANCIAL_MONTH
を、閏年として扱いたい場合はLEAP_YEAR
を、閏年以外として扱いたい場合はNON_LEAP_YEAR
を使います。
Time::HiRes ──ミリ秒まで時間を扱うモジュール
UNIX時間は秒数を単位にしているため、小数点以下の時間、つまりミリ秒やマイクロ秒をそのままでは扱えません。
Time::HiRes
モジュール(執筆時点のバージョンは1.9764)は、マイクロ秒まで扱うことができ、精度が求められる場合に有用です。現在時間をマイクロ秒までほしい場合には、このモジュールのgettimeofday
関数を使うと、UNIX時間と小数点以下の秒数が配列で返ってきます。
マイクロ秒までの時間の差は、tv_interval
関数にgettimeofday
関数で得られた時間を渡すことで得られます。第二引数を省略した場合は現在時間を参照します。
sleep
関数では秒数で実行を停止しましたが、Time::HiRes
モジュールではusleep
関数にマイクロ秒を渡すことで浮動小数点の秒数でプログラムの実行を停止できます。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT