テキストファイルの日付の変更スクリプト　〜Date::ManipとCalendar::Japanese::Holidayの活用〜

テキストファイルのあちらこちらにちりばめられている日付をまとめて、指定期間分シフトさせたい時ってありますよね。でも、単純作業なので、モチベーションは上がらないし、ミスする訳にもいかない。こんな時は、perlのモジュールを活用して、スクリプトに自動実行させちゃいましょう。

# 前準備 #

perlで日付を処理するなら、[Date::Manip](http://search.cpan.org/~sbeck/Date-Manip-6.48/lib/Date/Manip.pod)がお勧め。<br/>ただし、休日は[Date::Manip::Config](http://search.cpan.org/~sbeck/Date-Manip-6.48/lib/Date/Manip/Config.pod)に従って、事前にconfigファイルに休日を設定しておく必要があります。<br/>でも、日本の休日は、HappyMonday制度もあって日付が一意でないこと、シフト先の日付まで設定する必要があって、事前準備が面倒です。そこで、cpanで提供されている[Calendar::Japanese::Holiday](http://search.cpan.org/~kztomita/Calendar-Japanese-Holiday-0.03/lib/Calendar/Japanese/Holiday.pm)を活用することにします。

## モジュールのインストール ##

### インストール対象のモジュールについて ###

以下のモジュールを使用します。

1. [Date::Manip](http://search.cpan.org/~sbeck/Date-Manip-6.48/lib/Date/Manip.pod)

	ubuntuのパッケージをインストールします。
2. [Calendar::Japanese::Holiday](http://search.cpan.org/~kztomita/Calendar-Japanese-Holiday-0.03/lib/Calendar/Japanese/Holiday.pm)

	残念ながら、ubuntuのパッケージ提供がないので、cpanから直接インストールします。

### インストール方法 ###
#### [Date::Manip](http://search.cpan.org/~sbeck/Date-Manip-6.48/lib/Date/Manip.pod)のインストール ####
インストール方法は、以下となります。

`$ sudo apt-get install libdate-manip-perl`
	
Ex.インストール例<br/>
![installDateManip.png](https://qiita-image-store.s3.amazonaws.com/0/59920/0d801a9b-8c18-34bf-d923-1ae0ebd5d8d4.png)

#### [Calendar::Japanese::Holiday](http://search.cpan.org/~kztomita/Calendar-Japanese-Holiday-0.03/lib/Calendar/Japanese/Holiday.pm)のインストール ####
まず、cpanからモジュールをインストールするためのツールをインストールします。ツールには、設定と操作が簡単なcpanminusを使用することとします。<br/>インストール方法は、以下となります。

`$ sudo apt-get install cpanminus`

Ex.インストール例<br/>
![installCpanm.png](https://qiita-image-store.s3.amazonaws.com/0/59920/6e74af85-4701-2fca-a250-cb7ddb0f21c1.png)

そして、cpanminusを使って、[Calendar::Japanese::Holiday](http://search.cpan.org/~kztomita/Calendar-Japanese-Holiday-0.03/lib/Calendar/Japanese/Holiday.pm)をインストールします。

`$ sudo cpanm Calendar::Japanese::Holiday`

Ex.インストール例<br/>
![installJaHoliday.png](https://qiita-image-store.s3.amazonaws.com/0/59920/7ac5a695-6878-8d7a-868f-e4af9b998d2b.png)

# スクリプト #

## 使用方法 ##
スクリプトを作成する前に、そのインタフェースを考えます。引数には、日付を変更したいファイルと、日付のシフト量を日数で指定できるものとします。<br/>実際のSyntaxは、こんな感じでしょうか。

`shiftDay.pl 日付の変更対象ファイル 日付変更量`
	
なお、変更前後をdiffなどで確認したいので、変更前のファイル名に拡張子".org"を付与してバックアップするようにします。

## ソースコード ##

### 概要 ###
このスクリプトでは、変更対象の日付と変更後の日付の形式を以下に固定しています。

<table>
<tr><td>変更前</td><td>**4桁**年**1桁または2桁**月**1桁または2桁**日</td></tr>
<tr><td>変更後</td><td>**4桁**年**2桁**月**2桁**日</td></tr>
<tr></tr>
</table>

変換対象のテキストで、その他の形式を使用している場合は、以下の3箇所を変更してください。

1.  入力内容を形式を変更したい場合

	```unless ($line =~ /\d{4}年\d{1,2}月\d{1,2}日/) {```
	perlの正規表現を使用して、日付変更が不要な行を取り除いています。<br/>perlの正規表現については、`man perlre`を参照してください。

	```($err, %match) = $date->parse_format("(?<PRE>.*?)%Y年%f月%e日(?<POST>.*)", $line);```
	[Date::Manip](http://search.cpan.org/~sbeck/Date-Manip-6.48/lib/Date/Manip.pod)で、変更対象の日付を探しています。<br/>書式については、`man Date::Manip`の出力内容の**PRINTF DIRECTIVES**の項を参照してください。

2.  出力内容を形式を変更したい場合

	```$retStr .= $match{PRE} . $date->printf("%Y年%m月%d日");```
	printfの引数は、`man Date::Manip`の出力内容の**PRINTF DIRECTIVES**の項を参照してください。

では、スクリプトのソースコードを以下に示します(詳細は、コメントを参照してください)。

### 全体 ###
```perl:
#!/usr/bin/perl -w

use 5.14.1;
use strict;
use utf8;
use Date::Manip::Date;
use Calendar::Japanese::Holiday;
use File::Basename;
use autodie;

sub usage {
    say STDERR "$0 file-to-be-modified day-offset";
    exit 1;
}

# 引数チェック
#   引数が2つであること、かつ、
#   ファイルがあること、かつ、
#   第2引数が数値であること、かつ、
#   ディレクトリに書込み権があること
sub checkArg {
    unless (scalar @ARGV == 2 and
            -f $ARGV[0] and
            $ARGV[1] =~ /^\d+$/) {
        usage;
    }

    my ($file, $path) = fileparse $ARGV[0];
    usage unless -w $path;
}

# バックアップ作成
#   日付変更前のファイルに拡張子".org"を付けて、バックアップを作成。
#   すでにバックアップが存在している場合は、何もしない。
sub makeBackup {
    my $file = shift;
    my $org = $file . ".org";

    return $org if -f $org;

    rename $file, $org;
    return $org;
}

sub shiftDay {
    my ($line, $offset) = @_;

    # 変換対象のデータの確認。
    unless ($line =~ /\d{4}年\d{1,2}月\d{1,2}日/) {
        # yyyy年mm月dd日の文字列がなければ、変換不要。
        return $line;
    }

    my $retStr;
    my $date = new Date::Manip::Date;
    my $err;
    my %match;
    while(1) {
        # 1度に1つずつ、日付の変換を実施。
        #   日付部分のみを変更できるよう、日付前後のデータを、@match{PRE, POST}に保存。
        ($err, %match) = $date->parse_format("(?<PRE>.*?)%Y年%f月%e日(?<POST>.*)", $line);
        # ループ処理後の$lineに日付データがない場合、ループを終了。
        last if $err;
        
        # 指定日の変更
        #   まず、offset分進めたところの営業日を計算。
        #   次に、変更後の日付が、以下の条件を満たすまで、Date::Manipが認識する営業日を進める。
        #   - 営業日である、かつ、
        #   - 日本の休日でない条件が成立するまで、
        $date->next_business_day($offset, 0);
        my @d = $date->value;
        # 日本の休日の確認
        while(isHoliday($d[0],$d[1],$d[2], 1)) {
            $date->next_business_day(1, 0);
            @d = $date->value;
        }
        
        # 変換後の文字列への結合
        $retStr .= $match{PRE} . $date->printf("%Y年%m月%d日");
        
        # 残り部分を$lineに設定
        #   次のループで、変換対象の日付がないかを確認する。
        $line = $match{POST};
    }

    # 変換後の文字列への結合
    #   変換対象の日付を含まない、オジルナルの最終部分を結合する。
    $retStr .= $line;

    return $retStr;
}

checkArg;
binmode STDOUT, ":utf8";

my $org = makeBackup $ARGV[0];

# ファイルのオープン
open my $src, "<:utf8", $org;
open my $dest, ">:utf8", $ARGV[0];

while(my $line = <$src>) {
    # 1行取り出して、日付を変換。
    # 日付の変換後の行を、新しいファイルに出力。
    chomp $line;
    my $newLine = shiftDay $line, $ARGV[1];
    say $dest $newLine;
}
close $src;
close $dest;

```

## 実行例 ##

ファイル：sample.txtを変換してみます。

変換前の内容：
`$ cat sample.txt`

	建国記念日の3営業日後は、2015年2月11日ですね。また、ゴールデンウィーク開始後(4/29)の3営業日後は、2015年4月29日ですね。

変換操作：
`$ shiftDay.pl sample.txt 3`

変換後の内容：
`$ cat sample.txt`

	建国記念日の3営業日後は、2015年02月16日ですね。また、ゴールデンウィーク開始後(4/29)の3営業日後は、2015年05月07日ですね。

バックアップされたオリジナルファイル：
`$ cat sample.txt.org`

	建国記念日の3営業日後は、2015年2月11日ですね。また、ゴールデンウィーク開始後(4/29)の3営業日後は、2015年4月29日ですね。