sironekotoroの日記

Perl で楽をしたい

ある月の最後の平日を求める

ってことでこんにちは。休日になると仕事のスクリプト作成が捗りますね。

なぜか仕事のある平日はそうでもないのですが・・・

その月の最後の平日を求めたい理由

経理の世界では・・・と主語を大きく言いたいところですが、経理は各会社それぞれの色が強く出るところなので「今の業務では」と言っておきます。

まぁ、矛盾のない貸借対照表損益計算書が出てくれば、その過程は問われないというというのはあります。もちろん説明責任はあります。

今の業務では、ある月に発生した未払金(翌月払い等)は経理上、末日付で未払金計上します。クレカとか、AWSの利用料とか。

6月のAWS使用料金が20,000円だった場合の仕訳例です。未払金は翌月以降に支払うお金なので、末日が平日であろうが休日であろうがかまいません。

日付 借方 貸方 摘要
2020/06/30 支払手数料 20,000 未払金 20,000 AWS 6月分使用料

これを7月末に支払いすると、このような仕訳になります。この支払日は銀行営業日、つまり平日である必要があります。

銀行の通帳に記録される増減と仕訳上の記録を合わせておく必要があるためです。合わせないと照合作業が面倒すぎて死ねますね。

日付 借方 貸方 摘要
2020/07/31 未払金 20,000 普通預金 20,000 AWS 6月分使用料

この「その月の最後の平日」、支払の仕訳の「2020/07/31」を楽に求めたい!

特に経理ソフト上でコツコツ入力せず、csv作ってまとめて一発登録!みたいなのやっている自分だと特に!!

その月の最後の平日を求める

実際には Google Apps Script 、つまりJavaScript で書いてたんですが Perl のコードで書いてみます。

まず、日時を扱う Time::Piece で日時のオブジェクトを作成します。

オブジェクトは、「データと、そのデータを扱う関数が一緒になったもの」という説明をしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;

# 2020年7月でTime::Pieceオブジェクトを作る
my $t = Time::Piece->strptime("2020-07", '%Y-%m');

# 7月の最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 2020年7月最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("2020-07-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

2020年7月31日は金曜日なので $last_date->day_of_week の結果は 5 が返ります。

ふむふむ、つまり、最終日が日曜日( $last_date->day_of_week0)か土曜日( $last_date->day_of_week6)だったら前日を設定し、もう一回判定して・・・とやればいいな?と目星をつけます。

前日を設定するのは、 Time::Seconds モジュールの ONE_DAY 使いますかね。楽だし。

$yesterday = $today - ONE_DAY; こんな感じ。

なお、日本の休日&会社の休日は考慮しません。面倒なので・・・

で、最初に作ったのがこれです。while ループのところに安全策で count によるループ抜けを仕掛けておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

# 2020年7月でTime::Pieceオブジェクトを作る
my $t = Time::Piece->strptime("2020-07", '%Y-%m');

# 7月の最終日を求める
my $day = $t->month_last_day();
print "last day of month: ". $day . "\n";# 31

# 2020年7月最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("2020-07-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ". $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2020-07-31

7月は一見うまくいったように見えます・・・というか、7月末日は平日なので while ループ通りません。

2020年10月の末日は31日で土曜日です。30日が帰ってくれば大丈夫です。これでやってみましょう。

ついでに、年と月も変数にしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year = 2020;
my $month = 10;

my $t = Time::Piece->strptime("$year-$month", '%Y-%m');

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("$year-$month-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2020-10-30

大丈夫っすね。

これでいいかなー、というところですが、日曜日のパターンもやっておきましょう。

2021年1月は末日が31日で日曜日です。1月29日が帰ってくれば正解です。さて・・・?

あまり代わり映えしないので折りたたみ

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year = 2021;
my $month = 1;

my $t = Time::Piece->strptime("$year-$month", '%Y-%m');

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";# 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime("$year-$month-$day", '%Y-%m-%d');

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";# 5

my $count = 0;
while ($last_date->day_of_week == 0 || $last_date->day_of_week == 6){

    $last_date = $last_date - ONE_DAY;  # Time::Pieceオブジェクトを1日前にする

    if ($count > 7){
        last;
    }
    $count++;
}

print 'last normal day: '. $last_date->ymd . "\n";  # last normal day: 2021-12-31

大丈夫そうです。

それでも祝日とか会社の休みとかを考慮したい!

Perl のモジュールでも同様の動機で作られたモジュールがあります。そういうの使うのもいいと思います。

せっかくなので、先のコードを変えてみましょう。

対象は2020年の年末としますかねー。

12月の末日は31日ですが、会社は28日から休みって事にしておきますか。ホワイト会社だ!

この場合の最後の平日は12月25日になります。最高ですね。あ、でも経理の月末の仕事溜まって大変になりそうでダメだ(経理脳)

従来のコードは while ループに入る条件が、土曜日か日曜日というものだったので、これを変更するところから。

while を無限ループにして、条件によってループを抜ける、と変更します。大連休も考慮して、安全策のループ抜けは30日にしておきます。

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year  = 2020;
my $month = 12;

my $t = Time::Piece->strptime( "$year-$month", '%Y-%m' );

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";    # 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";    # 5

my $count = 0;
while (1) {

    if ( $last_date->day_of_week == 0 || $last_date->day_of_week == 6 ) {

        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
    }

    if ( $count > 30 ) {
        last;
    }
    $count++;
}

print 'last normal day: '
    . $last_date->ymd
    . "\n";               # last normal day: 2021-01-29

表示結果だけ見るとうまくいってるように見えますが、安全策のループがなかったら無限ループしています。

というわけで、ちゃんとループの終了判定入れて、スクリプト内に書いた祝日の日付があったら平日とみなさずに飛ばすようにしてみました。

もし、もっと手を掛けたくない!ってなったら、Google Calenderに「日本の祝日」「会社の祝日」のデータを登録して、それを引っ張ってくるとかやるかもしれません・・・が今日はやりません。

気休めのつもりが2時間くらいかかってしまってるので・・・

#!/usr/bin/env perl
use strict;
use warnings;

use Time::Piece;
use Time::Seconds qw/ONE_DAY/;

my $year  = 2020;
my $month = 12;

# 休日を設定
my $holiday = [ '2020-12-28', '2020-12-29', '2020-12-30', '2020-12-31' ];

my $t = Time::Piece->strptime( "$year-$month", '%Y-%m' );

# 最終日を求める
my $day = $t->month_last_day();
print "last day of month: ", $day . "\n";    # 31

# 最終日のTime::Pieceオブジェクトを作る
my $last_date = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );

# 最終日の曜日を求める
# 0: 日曜 〜 6: 土曜
print "day of week: ", $last_date->day_of_week . "\n";    # 5

my $count = 0;
while (1) {

    # 設定した休日と同じymdだったらnext
    if ( grep { $_ eq $last_date->ymd } @{$holiday} ) {
        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
        next;
    }
    elsif ( $last_date->day_of_week == 0 || $last_date->day_of_week == 6 ) {

        $last_date = $last_date
            - ONE_DAY;    # Time::Pieceオブジェクトを1日前にする
        next;
    }
    else {
        last;
    }

    if ( $count > 30 ) {
        last;
    }
    $count++;
}

print 'last normal day: '
    . $last_date->ymd
    . "\n";    # last normal day: 2020-12-25