tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

日付変更線(?)

日付の変わり目が、0時だと困ることがあります。

たとえば、この「はてなダイアリー」は、深夜にエントリを書く人(オイラみたいなコンピュータ屋さんに多いだろうね)を想定して、日付の変わり目がデフォルトで 5時(だっけ?)に設定されています。ほかにも、テレビ局なんかも、25時とか26時とか良く分からないタイムテーブルを持ってたりするよね?

オイラは工場勤務です。工場の人は交代制で働いています。なので、朝の交代の時間が日付の変わり目になるらしいです。

まあ日付の変わり目がいつも0時とは限らないよ、というのがこれでお分かりいただけたかと思います。

で、これをプログラミングでどう実現するか、というお話。

今日は 2/21(はてなダイアリー的には前述の理由で2/20だけど、一般人的には日付変わったから2/21だよね)ですが、交代制勤務で、交代の時間が仮に9:00としましょう。つまり「2/22 の 8:59 までは 2/21 として扱う」ということになります。

で、本日の日付をどう求めるかを考えていきます。

まずは、日付変更を無視して、一番簡単なケースを考えます。こんな感じ。

#!/usr/bin/perl
use strict;
use warnings;
use Test::More "no_plan";

sub today {
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  return sprintf("%04d/%02d/%02d", $year+1900, $mon+1, $mday);
}

is(today(), "2009/02/21");

実行します。

tsucchi@appears[108]% prove a.pl
a.......ok
All tests successful.
Files=1, Tests=1,  1 wallclock secs ( 0.00 usr  0.00 sys +  0.12 cusr  0.05 csys =  0.17 CPU)
Result: PASS

OK ですね。これだと明日になるとダメなので、Time::Fake を使っていつ実行しても結果が変わらんようにします。(see also: Time::Fake 可愛いよ、Time::Fake、 - tsucchiの日記)

#!/usr/bin/perl
use strict;
use warnings;
use Test::More "no_plan";
use Time::Local;
use Time::Fake;

sub today {
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  return sprintf("%04d/%02d/%02d", $year+1900, $mon+1, $mday);
}

Time::Fake->offset(timelocal(56, 34, 12, 21, 2-1, 2009));# 2009/2/21 12:34 56秒;
is(today(), "2009/02/21");

実行します。

tsucchi@appears[109]% prove a.pl
a.......ok
All tests successful.
Files=1, Tests=1,  0 wallclock secs ( 0.00 usr  0.02 sys +  0.15 cusr  0.00 csys =  0.17 CPU)
Result: PASS

まあ OK ですな。

では、日付の変わり目が 9:00 になるとします。テストはこんな感じかな。

#!/usr/bin/perl
use strict;
use warnings;
use Test::More "no_plan";
use Time::Local;
use Time::Fake;

sub today {
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  return sprintf("%04d/%02d/%02d", $year+1900, $mon+1, $mday);
}

Time::Fake->offset(timelocal(59, 59, 8, 21, 2-1, 2009));# 2009/2/21 8:59 59秒;
is(today(), "2009/02/20");

Time::Fake->offset(timelocal(0, 0, 9, 21, 2-1, 2009));# 2009/2/21 9:00 00秒;
is(today(), "2009/02/21");

8:59:59 では前日、9:00:00 では当日になってくれれば良い訳です。では実行します。

% prove a.pl
a.......1/?
#   Failed test at a.pl line 14.
#          got: '2009/02/21'
#     expected: '2009/02/20'
# Looks like you failed 1 test of 2.
a....... Dubious, test returned 1 (wstat 256, 0x100)
 Failed 1/2 subtests

Test Summary Report
-------------------
a.pl (Wstat: 256 Tests: 2 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=2,  0 wallclock secs ( 0.00 usr  0.02 sys +  0.14 cusr  0.01 csys =  0.17 CPU)
Result: FAIL

today() は現在日付を返しているので、当然失敗します。

で、ここからが難問。9:00という時間の境界をどう実現するか、を考えなければなりません。「localtime の戻り値の時間を見る」というのが一番最初に思いつきますが、どう考えてもダメダメです。仮に時間を見て日付を戻すにしても、月末月初の処理やうるう年の処理を考えると、悪夢以外の何者でもありません。

で、オイラはたまたま today() 側の処理に色々書いていたこともあって、ちょっとひらめきがありました。(全然たいしたことないんだけどさ)

「9:00開始ってことは、0:00が9:00にずれるってことじゃね?オフセット取ればよくね?」ということです。つーわけで、「オフセット」というアイデアを反映させます。

#!/usr/bin/perl
use strict;
use warnings;
use Test::More "no_plan";
use Time::Local;
use Time::Fake;

sub today {
  my ($offset) = @_;
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  return sprintf("%04d/%02d/%02d", $year+1900, $mon+1, $mday);
}

Time::Fake->offset(timelocal(59, 59, 8, 21, 2-1, 2009));# 2009/2/21 8:59 59秒;
is(today(9), "2009/02/20");

Time::Fake->offset(timelocal(0, 0, 9, 21, 2-1, 2009));# 2009/2/21 9:00 01秒;
is(today(9), "2009/02/21");

today()にオフセットとして、9時間を渡します。

tsucchi@appears[112]% prove a.pl
a.......1/?
#   Failed test at a.pl line 15.
#          got: '2009/02/21'
#     expected: '2009/02/20'
# Looks like you failed 1 test of 2.
a....... Dubious, test returned 1 (wstat 256, 0x100)
 Failed 1/2 subtests

Test Summary Report
-------------------
a.pl (Wstat: 256 Tests: 2 Failed: 1)
  Failed test:  1
  Non-zero exit status: 1
Files=1, Tests=2,  0 wallclock secs ( 0.00 usr  0.00 sys +  0.12 cusr  0.08 csys =  0.20 CPU)
Result: FAIL

渡したオフセットに対して、today() はまだ何もしていないから、当然テストは失敗します。

today() 側では、オフセット値を引き算するようにします。time() は秒単位なので、秒に変換する必要があります。こんな感じ。

sub today {
  my ($offset) = @_;
  my $offset_sec = $offset * 60 * 60;
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time - $offset_sec);
  return sprintf("%04d/%02d/%02d", $year+1900, $mon+1, $mday);
}

現在時刻( time() )から、$offset_sec を引き算すればよいわけです。で、実行します。

tsucchi@appears[113]% prove a.pl
a.......ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.00 usr  0.02 sys +  0.12 cusr  0.05 csys =  0.19 CPU)
Result: PASS

テストも無事通りました。というわけで終了です。