Perl日記

日々の知ったことのメモなどです。Perlは最近やってないです。

各月の平日の数を取得する

各月の平日の数を取得する。
つまりはウィークデイ。
(いままで土日を除いた平日を「ウィークデイ」だと思っていたけれど、日曜日だけ抜く使い方もあるらしい。weekdayの意味 - 英和辞書 - goo辞書)
(ここでは土日を除いた平日の数を出す)
とりあえず祝日は考慮しない。


CPANとかありそうだったけど、せっせと再発明で修行する。


ちょっと考えて、一日ずつ localtime と timelocal で曜日取得して判断しようかな、と思った。
こんな感じ。

use Time::Local qw/ timelocal /;

my $count = 0;
my $year  = 2010;
my $month =    4; # April
for my $day (1 .. 31) {
  my $wday = eval { (localtime(timelocal(0,0,0,$day,$month-1,$year)))[6] };
  if (defined($wday) and (0 != $wday) and (6 != $wday)) {
    $count++;
  }
}
say $count; # => 22


が、timelocal → localtime を12ヶ月なら365ないし366回しないといけない。
5年分なんかになったら約1500回だ。
しかも、ありえない日付対策で、それを毎回 eval させてる。
さすがにちょっと無駄があるなあ、と考えなおしてみる。


カレンダーとにらめっこして、おやと思う。
結局のところ「日」は、

  • 28日(2月)
  • 29日(2月うるう年)
  • 30日(4,6,9,11月)
  • 31日(1,3,5,7,8,10,12月)

のパターンしかない。


確実に月の始まりの「ついたち」は「日月火水木金土」のどれかで始まるから(そりゃそうだ)、それにあわせて平日の数が出せるのでは…?
計算するより、手で数えてみた。

my %COUNT_WEEKDAY = (
# firstday_wday
  Sun => {
  # lastday => count
    28 => 20,
    29 => 20,
    30 => 21,
    31 => 22,
  },
  Mon => {
    28 => 20,
    29 => 21,
    30 => 22,
    31 => 23,
  },
  Tue => {
    28 => 20,
    29 => 21,
    30 => 22,
    31 => 23,
  },
  Wed => {
    28 => 20,
    29 => 21,
    30 => 22,
    31 => 23,
  },
  Thu => {
    28 => 20,
    29 => 21,
    30 => 22,
    31 => 22,
  },
  Fri => {
    28 => 20,
    29 => 21,
    30 => 21,
    31 => 21,
  },
  Sat => {
    28 => 20,
    29 => 20,
    30 => 20,
    31 => 21,
  },
);

あれ? もうこれ使ったほうが簡単じゃね? ハッシュ最速伝説じゃね?
あとは、月の最初の曜日と、末日を取得するようにすれば完成?

sub wday_of_firstday {

  my @WDAY = qw/ Sun Mon Tue Wed Thu Fri Sat /;

  my $year  = shift;
  my $month = shift;

  my $time = timelocal(0, 0, 0, 1, $month - 1, $year);
  my $wday_of_firstday = (localtime($time))[6];

  return $WDAY[$wday_of_firstday];
}
sub lastday {

  my %LASTDAY = (
    1 => 31, 2 => 28, 3 => 31,  4 => 30,  5 => 31,  6 => 30,
    7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31,
  );

  my $year  = shift;
  my $month = shift;

  my $lastday = $LASTDAY{$month};

  if (2 == $month) {
    return is_leap($year) ? 1 + $lastday : $lastday;
  }
  else {
    return $lastday;
  }
}

is_leap()はうるう年判定。


あわせて、

my $year = 2010;
my $month = 4;

my $wday_of_firstday = wday_of_firstday($year, $month);
my $lastday = lastday($year, $month);

say $COUNT_WEEKDAY{$wday_of_firstday}->{$lastday};
# => 22

できました。

えらい力技になったけど、最初に比べたら計算量減っているので、まあ悪くはないかな。