sironekotoroの日記

Perl で楽をしたい

YAPC::Japan::Online 2022 懇親会での「和暦西暦変換」を実況中継風に

YAPC::Japan::Online 2022 から 2週間が経ちました

興奮冷めやらぬ・・・と言いたいのですが、興奮もそこそこに業務の嵐に飲み込まれた2週間でした。

で、やろうと思っていた Perl入学式プレゼンツのお題やるのすっかり忘れておりました。

和暦西暦変換

f:id:sironekotoro:20220319235847p:plain

こんな感じです。

早速やっていきます。もちろん、Perl 入学式の範囲で!

元号のスタート年は 元号一覧(日本)) を参考とし、以下とします。

  • 慶応:1865
  • 明治:1868
  • 大正:1912
  • 昭和:1926
  • 平成:1989
  • 令和:2019

まずはやってみる

まずは簡単に、愚直にやってみます。

テストケースは慶応1年(1865年)と慶応2年(1866年)です。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( keiou => 1865, );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /慶応(\w+)/ ) {
    my $start_year = $era{keiou};
    print $era{keiou} + $1 - 1;
}
$ perl wareki-seireki.pl 慶応1年
1865 

$ perl wareki-seireki.pl 慶応2年
1866  

順調ですね。

簡単に、とは言っていますが、漢字の年号を正しく正規表現で引っ掛けるために

use utf8;
use Encode qw/decode/;

しています。

ここは2021年から追加された Perl と日本語 でやったところです。追加しておいてよかったー

同じように明治も追加しておきます。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( keiou => 1865, meiji => 1868 );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /慶応(\w+)/ ) {
    my $start_year = $era{keiou};
    print $era{keiou} + $1 - 1;
}
elsif ( $input =~ /明治(\w+)/ ) {
    my $start_year = $era{meiji};
    print $era{meiji} + $1 - 1;
}
$ perl wareki-seireki.pl 明治1年
1868 

$ perl wareki-seireki.pl 明治2年
1869 

よかよかー

元年対応

このまま大正、昭和と追加していくか・・・というところで思い出します。

なんか、元年対応してたな、と。

元年対応を盛り込みます。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( keiou => 1865, meiji => 1868 );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /慶応(元|\w+)/ ) {
    my $start_year = $era{keiou};

    if ( $1 =~ /元|1/ ) {
        print $era{keiou};
    }
    else {
        print $era{keiou} + $1 - 1;
    }
}
elsif ( $input =~ /明治(元|\w+)/ ) {
    my $start_year = $era{meiji};
    if ( $1 =~ /元|1/ ) {
        print $era{meiji};
    }
    else {
        print $era{meiji} + $1 - 1;
    }
}
$ perl wareki-seireki.pl 慶応元年  
1865                                                                                                                               
$ perl wareki-seireki.pl 慶応1年
1865                                                                                                                               
$ perl wareki-seireki.pl 慶応2年
1866                                                                                                                               
$ perl wareki-seireki.pl 明治元年
1868                                                                                                                               
$ perl wareki-seireki.pl 明治1年 
1868                                                                                                                               
$ perl wareki-seireki.pl 明治2年
1869                                                                                                                               

大丈夫そうですね。

サブルーチンにまとめてみる

さて、このまま大正、昭和と追加して行ってもいいんですが、なんかコードに似たようなところがあります。

if 文の中身のところですね。

if ( $input =~ /慶応(元|\w+)/ ) {
    my $start_year = $era{keiou};

    if ( $1 =~ /元|1/ ) {
        print $era{keiou};
    }
    else {
        print $era{keiou} + $1 - 1;
    }
}
elsif ( $input =~ /明治(元|\w+)/ ) {
    my $start_year = $era{meiji};

    if ( $1 =~ /元|1/ ) {
        print $era{meiji};
    }
    else {
        print $era{meiji} + $1 - 1;
    }
}

似たようなところはサブルーチンにまとめるとコードの見通しもよくなりそう?

ってことでやってみます。

そして、ここに来て my $start_year = $era{keiou}; を全く使っていなかったことに気づきます・・・削除。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( keiou => 1865, meiji => 1868 );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /慶応(元|\w+)/ ) {
    calc( 'keiou', $1 );
}
elsif ( $input =~ /明治(元|\w+)/ ) {
    calc( 'meiji', $1 );
}

sub calc {
    my $era_name = shift;
    my $year     = shift;

    if ( $1 =~ /元|1/ ) {
        print $era{$era_name};
    }
    else {
        print $era{$era_name} + $1 - 1;
    }
}

まとまりましたね。

ここらで ok としましょうか・・・と言ったところで気づきます。

サブルーチンの中に $1 がある、ということに。

修正します。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( keiou => 1865, meiji => 1868 );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /慶応(元|\w+)/ ) {
    calc( 'keiou', $1 );
}
elsif ( $input =~ /明治(元|\w+)/ ) {
    calc( 'meiji', $1 );
}

sub calc {
    my $era_name = shift;
    my $year     = shift;

    if ( $year =~ /元|1/ ) {
        print $era{$era_name};
    }
    else {
        print $era{$era_name} + $year - 1;
    }
}

まだまだ!

そして、まだ同じようなコードが見えます。

if ( $input =~ /慶応(元|\w+)/ ) {
    calc( 'keiou', $1 );
# 中略
elsif ( $input =~ /明治(元|\w+)/ ) {
    calc( 'meiji', $1 );

変化しているのは 慶応keiou明治meiji だけで、あとは同じコードだよね・・・?

というわけで、もう元号を直接ハッシュのkeyに設定してしまいます。こんな感じで。

my %era = ( 慶応 => 1865, 明治 => 1868 );

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = ( 慶応 => 1865, 明治 => 1868 );

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /(\w{2,2})(元|\w+)/ ) {
    calc( $1, $2 );
}

sub calc {
    my $era_name = shift;
    my $year     = shift;

    if ( $year =~ /元|1/ ) {
        print $era{$era_name};
    }
    else {
        print $era{$era_name} + $year - 1;
    }
}

最後に

これだけ短くなると、もうサブルーチン必要ないのでは?となるので、まとめてしまいます。

それと、元年対応のところの正規表現 /(元|\w+)年/ ) も重複してるので、こちらも整理。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = (
    慶応 => 1865,
    明治 => 1868,
    大正 => 1912,
    昭和 => 1926,
    平成 => 1989,
    令和 => 2019,
);

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /(\w{2,2})(\w+)/ ) {

    if ( $2 eq '元' || $2 == 1 ) {
        print $era{$1};
    }
    else {
        print $era{$1} + $2 - 1;
    }

}

Perl入学式範囲外 だったら?

正規表現には名前付きキャプチャというものがあります。これを使って $1, $2 より可読性の高いコードにします。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = (
    慶応 => 1865,
    明治 => 1868,
    大正 => 1912,
    昭和 => 1926,
    平成 => 1989,
    令和 => 2019,
);

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /(?<era>\w{2,2})(?<year>\w+)/ ) {

    if ( $+{year} eq '元' || $+{year} == 1 ) {
        print $era{$1};
    }
    else {
        print $era{ $+{era} } + $+{year} - 1;
    }

}

とはいえ、3ヶ月後にこのコードを見た自分はちゃんと理解できるだろうか・・・?

プログラム書いてると、自分の馬鹿さ加減が身に染みてるわけです。

というわけで、ここまで書いてきてなんですが、理解を優先して一番最初の愚直なコードにすることもあるかもです。

さらに

やっぱ、漢数字対応はほしいよね?と思って手を出したら、こっちの方が時間かかりましたね・・・よくあることです。

コードと実行結果

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode/;

my %era = (
    慶応 => 1865,
    明治 => 1868,
    大正 => 1912,
    昭和 => 1926,
    平成 => 1989,
    令和 => 2019,
);

my %kanji_num = (
    一 => 1,
    二 => 2,
    三 => 3,
    四 => 4,
    五 => 5,
    六 => 6,
    七 => 7,
    八 => 8,
    九 => 9,
    十 => 10,
);

my $input = decode( 'utf8', $ARGV[0] );

if ( $input =~ /^(?<era>\w{2,2})(?<year>\w+)年$/ ) {

    my $era_name = $+{era};
    my $year     = $+{year};

    if ( $year =~ /一|二|三|四|五|六|七|八|九|十/ ) {
        print $era{$era_name} + kanji_num($year) - 1;
    }
    elsif ( $year eq '元' || $year == 1 ) {
        print $era{$1};
    }
    else {
        print $era{$era_name} + $year - 1;
    }
}

sub kanji_num {
    my $kan_suuji = shift;

    # 10の桁、1の桁がある場合(ex.二十二)
    if ( $kan_suuji =~ /^(\w)(\w)$/ ) {
        return $kanji_num{$1} * 10 + $kanji_num{$1};
    }

    # 10の桁が1の場合(ex.十二)
    elsif ( $kan_suuji =~ /^十(\w)$/ ) {
        return 10 + $kanji_num{$1};
    }

    # 10の桁のみ場合(ex.二十)
    elsif ( $kan_suuji =~ /^(\w)十$/ ) {
        return $kanji_num{$1} * 10;
    }

    # 1桁のみと十のみの場合(ex.二)
    elsif ( $kan_suuji =~ /^(\w)$/ ) {
        return $kanji_num{$1};
    }
}