国民の祝日の名称を取得するモジュール

昨日の続き。

Calendar::Japanese::Holidayなるものを触ってみた - Unknown::Programming

いや、続きというか昨日も言ったように僕も前に作ったやつがあるので折角だし公開しよーかなと。

DateTime::Holiday::Japaneseという名前で作ってあるんだけど、名前空間的にDateTime使ってるということもありその辺良くわかんないので、CPANにはあげてません。

DateTime-Holiday-Japanese-0.01.tar.gz

ソースは以下

package DateTime::Holiday::Japanese;
use strict;
use warnings;
use DateTime;
use UNIVERSAL::require;
use Class::Inspector;
use utf8;
use base qw/Exporter/;

our @EXPORT_OK = qw/
    is_holiday
    is_holiday_name
    holiday_name
    holiday_english_name
    holidays
/;

our $VERSION = '0.01';

our %FIX_HOLIDAY = (
    '0101' => [[1949,0   ,'元日'        ]],
    '0115' => [[1949,1999,'成人の日'    ]],
    '0211' => [[1967,0   ,'建国記念の日']],
    '0429' => [
                [2007,0   ,'昭和の日'  ],
                [1989,2006,'みどりの日'],
                [1949,1988,'天皇誕生日'],
              ],
    '0503' => [[1949,0   ,'憲法記念日'  ]],
    '0504' => [[2007,0   ,'みどりの日'  ]],
    '0505' => [[1949,0   ,'こどもの日'  ]],
    '0720' => [[1996,2002,'海の日'      ]],
    '0915' => [[1966,2002,'敬老の日'    ]],
    '1010' => [[1966,1999,'体育の日'    ]],
    '1103' => [[1948,0   ,'文化の日'    ]],
    '1123' => [[1948,0   ,'勤労感謝の日']],
    '1223' => [[1989,0   ,'天皇誕生日'  ]],
);

our %SPECIAL_HOLIDAY = (
    '19590410' => '皇太子明仁親王の結婚の儀',
    '19890224' => '昭和天皇の大喪の礼',
    '19901112' => '即位礼正殿の儀',
    '19930609' => '皇太子徳仁親王の結婚の儀',
);

our %ETC_HOLIDAY = (
    ComingOfAge       => '成人の日',
    VernalEquinox     => '春分の日',
    NationalHoliday   => '国民の休日',
    Marine            => '海の日',
    RespectForTheAged => '敬老の日',
    AutumnalEquinox   => '秋分の日',
    HealthAndSports   => '体育の日',
    SubstituteHoliday => '振替休日',
);

our $TO_ENGLISH = {
    '元日'                     => 'New Year\'s Day',
    '成人の日'                 => 'Coming of Age Day',
    '建国記念の日'             => 'National Foundation Day',
    '昭和の日'                 => 'Showa Day',
    'みどりの日'               => 'Greenery Day',
    '天皇誕生日'               => 'The Emperor\'s Birthday',
    '憲法記念日'               => 'Constitution Memorial Day',
    'こどもの日'               => 'Children\'s Day',
    '海の日'                   => 'Marine Day',
    '敬老の日'                 => 'Respect for the Aged Day',
    '体育の日'                 => 'Health and Sports Day',
    '文化の日'                 => 'National Culture Day',
    '勤労感謝の日'             => 'Labor Thanksgiving Day',
    '春分の日'                 => 'Vernal Equinox Day',
    '秋分の日'                 => 'Autumnal Equinox Day',
    '国民の休日'               => 'National holiday',
    '振替休日'                 => 'Substitute holiday',
    '皇太子明仁親王の結婚の儀' => 'The Rite of Wedding of HIH Crown Prince Akihito',
    '昭和天皇の大喪の礼'       => 'The Funeral Ceremony of Emperor Showa.',
    '即位礼正殿の儀'           => 'The Ceremony of the Enthronement of His Majesty the Emperor (at the Seiden)',
    '皇太子徳仁親王の結婚の儀' => 'The Rite of Wedding of HIH Crown Prince Naruhito',
};

our $DATETIME_CLASS = 'DateTime';

our $HOLIDAY_TABLE = {}; # キャッシュ用

sub _get_dt { 
    return shift if @_ == 1;
    unless ( Class::Inspector->loaded($DATETIME_CLASS) ) {
        $DATETIME_CLASS->require or die $@;
    }
    return $DATETIME_CLASS->new( time_zone => 'Asia/Tokyo' , @_ );
}

sub _get_instance {
    DateTime::Holiday::Japanese->_new(@_);
}

sub _new {
    my $class = shift;
    bless { dt => _get_dt(@_) } , ref $class || $class;
}

sub dt { shift->{dt} }

sub is_holiday {
    my $self = _get_instance(@_);
    return 1 if $self->dt->day_of_week == 7;
    return $self->_holiday ? 1 : 0;
}

sub is_holiday_name {
    my $self = _get_instance(@_);
    return $self->_holiday ? 1 : 0;
}

sub holiday_name         { _get_instance(@_)->_holiday }
sub holiday_english_name {
    my $name = _get_instance(@_)->_holiday;
    return $name ? $TO_ENGLISH->{$name} : ();
}

sub holidays {
    my ($s_year,$e_year) = @_;
    $e_year ||= $s_year;
    my $s_dt  = _get_dt( year => $s_year , month => 1  , day => 1  );
    my $e_ymd = _get_dt( year => $e_year , month => 1  , day => 1  )->add( years => 1 )->ymd;
    
    my %hash;
    
    while ( $s_dt->ymd ne $e_ymd ) {
        if ( my $holiday = holiday_name($s_dt) ) {
            $hash{$s_dt->year} ||= {};
            $hash{$s_dt->year}->{$s_dt->strftime('%m-%d')} = $holiday;
        }
        $s_dt->add( days => 1 );
    }
    
    return \%hash;
}

sub _holiday {
    my $self = shift;
    if ( $HOLIDAY_TABLE->{$self->dt->year} ) {
        return $HOLIDAY_TABLE->{$self->dt->year}->{$self->dt->strftime('%m-%d')};
    }
    return
        $self->_basic_holiday || 
        $self->_change_holiday || 
        $self->_between_holiday || 
        $self->_special_holiday;
};

### 国民の休日、振替休日、特殊な休日、日曜日以外の全て休日
sub _basic_holiday {
    my $self = shift;
    
    return $ETC_HOLIDAY{VernalEquinox  } if $self->dt->ymd('') eq $self->_vernal_equinox;
    return $ETC_HOLIDAY{AutumnalEquinox} if $self->dt->ymd('') eq $self->_autumnal_equinox;
    
    if( $FIX_HOLIDAY{$self->dt->strftime('%m%d')} ) {
        for my $range (@{$FIX_HOLIDAY{$self->dt->strftime('%m%d')}}) {
            if( $self->dt->year >= $range->[0] && (!$range->[1] || $self->dt->year <= $range->[1] )) {
                return $range->[2];
            }
        }
    }
    
    return $self->_float_holiday;
};

### 変動する休日
### 今の所ハッピーマンデー系休日のみ
sub _float_holiday {
    my $self = shift;
    my $dt = $self->dt;
    return 
        ( $dt->month == 1  && $dt->day_of_week == 1 && $dt->weekday_of_month == 2 && $dt->year >= 2000 ) ? $ETC_HOLIDAY{ComingOfAge}       :
        ( $dt->month == 7  && $dt->day_of_week == 1 && $dt->weekday_of_month == 3 && $dt->year >= 2003 ) ? $ETC_HOLIDAY{Marine}            :
        ( $dt->month == 9  && $dt->day_of_week == 1 && $dt->weekday_of_month == 3 && $dt->year >= 2003 ) ? $ETC_HOLIDAY{RespectForTheAged} :
        ( $dt->month == 10 && $dt->day_of_week == 1 && $dt->weekday_of_month == 2 && $dt->year >= 2000 ) ? $ETC_HOLIDAY{HealthAndSports}   : ();
};

### 国民の休日
sub _between_holiday {
    my $self = shift;
    
    return if $self->dt->ymd('') < '19851227';
    return if $self->dt->day_of_week == 7;
    return if $self->_change_holiday;
    
    my $prev = _get_instance($self->dt->clone->subtract( days => 1 ));
    return if !$prev->_basic_holiday;
    
    my $next = _get_instance($self->dt->clone->add( days => 1 ));
    return if !$next->_basic_holiday;
    
    return $ETC_HOLIDAY{NationalHoliday};
};

### 振替休日
sub _change_holiday {
    my $self = shift;
    
    return unless $self->dt->ymd('') >= '19730412';
    
    my $dt = _get_instance($self->dt->clone);
    while( $dt->dt->subtract( days => 1 ) ){
        return unless $dt->_basic_holiday;
        return $ETC_HOLIDAY{SubstituteHoliday} if $dt->dt->day_of_week == 7;
        return if $self->dt->year < 2007;
    }
    return;
};

### 特別な休日
sub _special_holiday {
    my $self = shift;
    $SPECIAL_HOLIDAY{$self->dt->ymd('')};
};

### 春分の日を求める
sub _vernal_equinox {
    my $self = shift;
    my $year = $self->dt->year;
    
    my($x, $y);
    if (1900 <= $year && $year <= 1979) {
        $x = 20.8357; $y = 1983.0;
    }
    elsif(1980 <= $year && $year <= 2099) {
        $x = 20.8431; $y = 1980.0;
    }
    elsif(2100 <= $year && $year <= 2150) {
        $x = 21.8510; $y = 1980.0;
    }
    else {
        return '';
    }
    
    my $day = int($x + 0.242194 * ($year - 1980) - int(($year - $y) / 4));
    return sprintf '%04d%02d%02d',$year,3,$day;
};

### 秋分の日を求める
sub _autumnal_equinox {
    my $self = shift;
    my $year = $self->dt->year;
    
    my($x, $y);
    if (1900 <= $year && $year <= 1979) {
        $x = 23.2588; $y = 1983.0;
    }
    elsif(1980 <= $year && $year <= 2099) {
        $x = 23.2488; $y = 1980.0;
    }
    elsif(2100 <= $year && $year <= 2150) {
        $x = 24.2488; $y = 1980.0;
    }
    else {
        return '';
    }
    
    my $day = int($x + 0.242194 * ($year - 1980) - int(($year - $y) / 4));
    return sprintf '%04d%02d%02d',$year,9,$day;
};

1;

5つの関数をExportして使います。

引数にはDateTimeのnewと同じものが渡せるほか、DateTimeオブジェクトを渡すこともできます。

オブジェクトを渡した場合はそのオブジェクトを利用して処理を行います。

  • is_holiday

渡された日付が祝日(日曜日も含む)であるか判定します。

 use DateTime::Holiday::Japanese qw/is_holiday/;

 if ( is_holiday( year => 1999 , month => 5 , day => 5 ) ) {
     # 祝日
 }
  • is_holiday_name()

渡された日付が祝日(日曜日を含まない)であるか判定します。

  • holiday_name()

渡された日付が祝日だった場合に、その名称を返します。文字コードはフラグ付きのUTF8です。

  • holiday_english_name()

英語の名前を返します。

おまけ機能、というかコレ作った時に必要だったので用意しました。

蛇足感は否めないけどCPANにうpしてるわけじゃないのでご愛嬌。

  • holidays()

コレだけ引数が違います。

渡された二つの年から全ての祝日をハッシュのリファレンスで返します。

 use DateTime::Holiday::Japanese qw/holidays/;

 my $hash = holidays(1949,1950);
 # $hash = {
 #    1949 => {
 #        '01-01' => '元日',
 #        '01-15' => '成人の日',
 #        '03-21' => '春分の日',
 #        '04-29' => '天皇誕生日',
 #        '05-03' => '憲法記念日',
 #        '05-05' => 'こどもの日',
 #        '09-23' => '秋分の日',
 #        '11-03' => '文化の日',
 #        '11-23' => '勤労感謝の日',
 #    },
 #    1950 => {
 #        '01-01' => '元日',
 #        '01-15' => '成人の日',
 #        '03-21' => '春分の日',
 #        '04-29' => '天皇誕生日',
 #        '05-03' => '憲法記念日',
 #        '05-05' => 'こどもの日',
 #        '09-23' => '秋分の日',
 #        '11-03' => '文化の日',
 #        '11-23' => '勤労感謝の日',
 #    }
 # };

こんな感じです。

このholidays関数の用途としては予め1949年〜2030年くらいまでの祝日データを作っておいてメモリ上の何処かに保存しておいて祝日の処理を高速化することにあります。

例として、キャッシュがあればそれを使い、無ければ動的に取得するコードを書くなら以下。

 my $name;
 
 if ( $CACHE->{$dt->year} ) {
     $name = $CACHE->{$dt->year}->{$dt->strftime('%m-%d')}; 
 }
 else {
     $name = holiday_name($dt);
 }

 print $name;

$CACHEにholidays関数で作ったハッシュデータが入ってるとして、こんな感じでしょうかね。

このキャッシュの仕組みは一応内部でも組み込まれています。

$HOLIDAY_TABLEというパッケージ変数を用意しているのでそこにデータを突っ込んでおけばそっちを見てくれるような実装になってます。

まぁとりあえずholidays関数使って予め数年分のデータを用意しとけばDateTime::Holiday::Japaneseに依存しなくてよくなるのでそーゆー用途にどうぞ。

さて、こんなもんかな。

でもこういう将来変更される可能性のあるモジュールってずっと対応していくのが面倒ですよねぇ。また新しい祝日追加されたよ〜みたいな。