koluku's blog

毎日はcodeの欠片

🕗Perlの時間はモゞュヌルで #perlwakate

2020/07/01 にオンラむンで開催された 「Perl若手の䌚(https://connpass.com/event/179235/)」でのLT資料です。

speakerdeck.com

䞋蚘の文章はスラむド内容を䞀郚修正したものです。

匕甚の䞭は特にURLが無い限りはperldoc.jpからの匕甚です。

Perlでの基本実装

$^T倉数、time関数

Perlには$^Tずいう倉数が予め宣蚀されおいお、

プログラムを実行開始した時刻を、玀元 (1970幎の始め) からの秒数で 瀺したものです

ず定矩されおいたす。この数字は玀元 (1970幎の始め)からの秒数です。

$ perl -E 'say $^T'
1593654027

じゃあ玀元 (1970幎の始め) っお䜕なのかいうず、time関数に解説がありたす。

ほずんどのシステムでは玀元はUTC1970幎1月1日00:00:00です;特城的な䟋倖ずしおは、叀いMacOSではロヌカルタむムゟヌンの1904幎1月1日00:00:00を玀元ずしお䜿いたす。

UTC1970幎1月1日00:00:00はいわゆるUNIX時間なので基準ずしおわかりやすいです。

本圓に䟋倖ずしお叀いMacPower PCのMacintoshあたりのずきだけUTC1904幎1月1日00:00:00が開始になりたす。なぜ1904幎なのかずいうず、うるう幎の䟋倖回避のための日付だからです。

そもそもうるう幎ずは、

  1. 西暊幎が4で割り切れる幎は(原則ずしお)閏幎。
  2. ただし、西暊幎が100で割り切れる幎は(原則ずしお)平幎。
  3. ただし、西暊幎が400で割り切れる幎は必ず閏幎。

閏年 - Wikipedia

ず定矩されおいたす。

なので1900幎は100幎に䞀床の䟋倖でうるう幎が発生せず、2000幎は400幎に䞀床の䟋倖でうるう幎が発生したす。぀たり1904幎から始めるず2096幎たで4幎に䞀床うるう幎が存圚するので、うるう幎が発生しないずいう䟋倖凊理をしないで枈みたす。

$^T倉数もtime関数もどちらもシステム時間を返すのでどちらを䜿っおも同じです。倚分time関数のほうが読みやすいのでそちらのほうが良いかず思いたす。

localtime関数, gmtime関数

time関数は珟圚時間を知ろうにもUNIX時間秒数で返されるので普通には扱えないずいう問題がありたす。

そこでtime関数から個別の時刻情報を取り出すこずができるlocaltime関数がありたす。

time関数が返す時刻を、ロヌカルなタむムゟヌンで枬った時刻ずしお、9芁玠の配列に倉換したす。

my @localtime =  localtime;
say $localtime[2];

# OR

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

垰っおくる配列の䞭身はindex 0から順にこうなっおいたす。

  1. $sec 
 秒
  2. $min 
 分
  3. $hour 
 時間
  4. $mday 
 日
  5. $mon 
 月
  6. $year 
 幎
  7. $wday 
 曜日
  8. $yday 
 1幎の䞭で䜕日目
  9. $isdst 
 倏時間かどうか

ただし、幎の扱いがそのたたのグレゎリオ暊で出力されたせん。2020幎だず$yearは120で返っおきたす。

$year は 1900 幎からの幎数を持ちたす。

1900幎は人間が読みやすいように䞋二桁で刀断できる開始地点です。いわゆるPOSIX準拠ですね。なので1900を足しおから扱いたす。

スカラヌのずきに扱うず日付情報含めお文字列で出力されたす。

$ perl -E 'say scalar localtime'
Thu Jul  2 12:26:19 2020

POSIX::strftimeを䜵甚しお䜿うず任意の出力をするこずができたす。

use POSIX qw(strftime);
my $now_string = strftime "%a %b %e %H:%M:%S %Y", localtime

localtimeだずマシンの時間に䟝存するため日本だずGMT+9:00でグリニッゞ暙準時から9時間足された時間になりたす。

これがマシンの時間がわかっおいるのであれば単玔に-9時間で蚈算すればいいのですが、サヌバヌに眮いた堎合に時間わからない堎合があり、戻す時間がわからなくなりたす。

そこで最初からグリニッゞ暙準時で出力されるgmtime関数がありたす。こちらはlocaltimeず同じAPIで取り扱うこずができたす。

モゞュヌルでの実装

Time::Piece

localtimeは䟿利ですが、䜿いたい圢にするためには結局関数化する必芁がありたす。日付や時刻情報などは出力の圢が倧䜓決たっおおり、その圢にわざわざ曞くのも面倒です。

そこでTime::Pieceの登堎です。Time::Pieceはlocaltime関数ずgmtime関数をTime::Pieceオブゞェクトにしお返しおくれたす。ちなみにTimepieceは時蚈ずいう意味です。

分や日など単独で欲しい情報のメ゜ッドはもちろん、日付やフォヌマットの仕方などもメ゜ッドで提䟛されおいたす。

$t->ymd                 # 2000-02-29
$t->date                # $t->ymd ず同じ
$t->mdy                 # 02-29-2000
$t->mdy("/")            # 02/29/2000
$t->dmy                 # 29-02-2000
$t->dmy(".")            # 29.02.2000
$t->datetime            # 2000-02-29T12:34:56 (ISO 8601)
$t->cdate               # Tue Feb 29 12:34:56 2000

$t->time_separator($s)  # デフォルトのセパレヌタを蚭定 (デフォルト ":")
$t->date_separator($s)  # デフォルトのセパレヌタを蚭定 (デフォルト "-")
$t->day_list(@days)     # デフォルトの曜日を蚭定
$t->mon_list(@days)     # デフォルトの月名を蚭定

Time::Pieceオブゞェクトに察しおTime::Secondsを加算・枛算したり、Time::Pieceオブゞェクト同士で差分をTime::Secondsで出したりするこずができたす。

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

my $today = localtime;
my $tommorow = $today + ONE_DAY; # ONE_DAYはTime::Secondsで定矩されおいる1日
say $tommorow->ymd; # Time::Pieceオブゞェクト
# 2020-07-03

my $diff = $tommorow - $today;
say $diff->hours; # Time::Secondsオブゞェクト
# 24

たた、Time::Pieceオブゞェクト同士で比范する事もできたす。

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

my $today = localtime;
my $tommorow = $today + ONE_DAY;

$tommorow += ONE_HOUR;
if ($tommorow - $today != 24 * ONE_HOUR) {
    say '差は24時間ではないです';
}

しかし、䞀぀問題があり

このモゞュヌルは、perl の time() 経由で提䟛され、gmtime() ず localtime() が察応しおいる玀元秒システムを内郚で䜿甚しおいたす。

そもそもtime関数で拟っおいるtime_t型は笊号付きintで実装されおいるため231-1(2,147,483,647秒 = UTC2038幎01月19日03:14:07を超えるず負になり、 正しく時刻を扱うこずができなくなりたす。

そのため、回避策を取る必芁がありたす。

64 ビット perl を䜿っおください。 たたはこれらの遞択肢を取れない堎合は、過去ず未来の幎に察応しおいる DateTime モゞュヌルを䜿っおください

ほずんどの堎合は64bitのPerlで解決したすが、叀いPerlを䜿わないずいけない堎合などではDateTimeで察凊する必芁がありたす。

DateTimeでできるのであればTime::Pieceの優䜍性が無いず思われたすが、Time::PieceはPerl 5.10以降でデフォルトで入っおいるこずやDateTimeより軜いずいうこずもあり特に理由がなければTime::Pieceを䜿うのが良いず思いたす。

でもどうやっお加算や比范をしおる

ちょっず寄り道ですが、先皋Time::Pieceでは加算・枛算・比范ができるず蚀いたしたが、冷静に考えるずこれはちょっずおかしいです。ずいうのも、Time::Pieceはオブゞェクトなので数倀や文字列のように盎接比范するこずができたせん。

䞍思議だなぁずいうこずでmeta::cpanからBrowseでコヌドを芋おるずTime::Seconndsにoverloadパッケヌゞで挔算子を囲っおいるこずがわかりたす。

use overload
    'fallback' => 'undef',
    '0+' => \&seconds,
    '""' => \&seconds,
    '<=>' => \&compare,
    '+' => \&add,
    '-' => \&subtract,
    '-=' => \&subtract_from,
    '+=' => \&add_to,
    '=' => \&copy;
};

Seconds.pm - metacpan.org

overloadパッケヌゞずはなんぞやずperldoc.jpを芋おみるず、

Perl の挔算子のオヌバヌロヌドを行うパッケヌゞ

ずありたす。぀たり算術挔算子や比范挔算子を別の関数に䞊曞きできるみたいです。

Time::Secondsでは䟋えば+算術挔算子の堎合は巊右から倀を取っおadd関数に入れ、加算したものをTime::Seconndsで返しおいるこずがわかりたす。

sub _get_ovlvals {
    my ($lhs, $rhs, $reverse) = @_;
    $lhs = $lhs->seconds;
 
    if (UNIVERSAL::isa($rhs, 'Time::Seconds')) {
        $rhs = $rhs->seconds;
    }
    elsif (ref($rhs)) {
        die "Can't use non Seconds object in operator overload";
    }
 
    if ($reverse) {
        return $rhs, $lhs;
    }
 
    return $lhs, $rhs;
}

# ~

sub add {
    my ($lhs, $rhs) = _get_ovlvals(@_);
    return Time::Seconds->new($lhs + $rhs);
}

Seconds.pm - metacpan.org

Time::Piece::MySQL

時間ずいえばMySQLでの扱いはなかなかに面倒です。

䟋えば、時刻の衚蚘ではfrac小数点以䞋が远加されおいたす。

たた、DATETIMEずTIMESTAMPは同じフォヌマットでもサポヌトされおいる時間の範囲が異なり、 DATETIMEは月・日に00を蚱容されたす。そしお、時には同じ倀でも違う倀で登録されるこずがありたす。

そこで、Time::Pieceの拡匵メ゜ッドずしおTime::Piece::MySQLがありたす。

Time::Piece->mysql_dateでTime::PieceオブゞェクトをMySQLのDATE型に倉換し、Time::Piece->from_mysql_date($datetime)でMySQLのDATE型からTime::Pieceオブゞェクトに倉換するこずができたす。DATETIME・TIME・TIMESTAMPでも察応するメ゜ッドを䜿えば倉換するこずができたす。

Time::HiRes

そういえば時間ずいえば面倒なものがありたしたね。そうです、小数点以䞋の時間の扱いです。

time関数では秒数しか扱わないため、ミリ秒・マむクロ秒を扱うこずができたせん。

Time::HiRes::gettimeofdayを䜿うずtime関数で返っおくる秒数情報に加えおマむクロ秒が配列で返っおきたす。

use Time::Piece;
use Time::HiRes;

my ($time, $ms) = Time::HiRes::gettimeofday;
my $t = localtime($time);
say $t->datetime . ".$ms";
# 2020-07-02T19:31:40.946569