sironekotoroの日記

Perl と Mac の初心者の備忘録

Perl入学式の小ネタ:正規表現の文字境界

前振り

あなたはあるブログプラットフォームの開発者です。

昨今流行している新型コロナウイルス「COVID-19」について投稿される記事も多くなってきました。

掲載される情報は正しいものもありますが、アクセス数狙いの過激な文言を含む記事もあります。

そのため、COVID-19に関する記事については画面の上部に「信頼しうるソースへのリンク」を掲示することになりました。

開発者は「covid」「COVID-19」「コロナウイルス」「新型コロナ」「発熱」など、いくつかの単語を用意し、記事を正規表現で検索します。

検索結果にマッチした記事だった場合、画面に注意書きを掲載する、という仕組みを作りました。

今回はCOVIDという文字を含む記事、という想定で書いてみました。判定には以下の正規表現を用います。

$entry =~ /COVID/i 
#!/usr/bin/env perl
use strict;
use warnings;

my @entries = (
    'No.1: covid-19の症状について',
    'No.2: COVID-19の症状について',
    'No.3: COVID19の症状について',
    'No.4: COVIDの症状について',
);

for my $entry (@entries) {
    if ( $entry =~ /COVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: No.1: covid-19の症状について
# WARNING!: No.2: COVID-19の症状について
# WARNING!: No.3: COVID19の症状について
# WARNING!: No.4: COVIDの症状について

Perl入学式の第3回で学習した範囲です。 /i とオプションをつけることで、大文字小文字にかかわらずマッチさせることが可能です。

しかし・・・?

実装後、いくつかのCOVID-19とは関係ない記事に警告が表示されているとの報告がユーザーから寄せられました。

調査の結果、ニコニコ動画のURLを含む記事も正規表現にマッチする結果となっていることが判明しました。

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

my @entries
    = (
    '<a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>'
    );

for my $entry (@entries) {
    if ( $entry =~ /COVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: <a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>

なるほど、URLの一部に covid という語が含まれています。

本来のCOVID という語のみを検出し、niconico動画のURLを除外するためにはどうすれば良いでしょうか?

文字境界の /b

正規表現の対象語の前後(あるいは必要なところ)に '/b' を置くことで、より精緻にマッチさせることが可能になります。

\b Match a word boundary

文字の境界とはなんぞや?というと下記のリンク先にも書いてありますが、こういうことです。

単語境界(\b)は\W にマッチングする文字列の始まりと終わりを 連想するような、片側を \w、もう片側を \W で挟まれている点です。

perldoc.jp

\w\W についてはPerl入学式でも解説してあります。

\w ... アルファベット, 数字, アンダーバーの1文字 [a-zA-Z0-9_]と同じ意味です.

\W ... アルファベット, 数字, アンダーバー以外の1文字 [^a-zA-Z0-9_]と同じ意味です.

github.com

以上を踏まえて、正規表現を書き直してみます。

$entry =~ /\bCOVID/i 
#!/usr/bin/env perl
use strict;
use warnings;

my @entries
    = (
    'No.1: covid-19の症状について',
    'No.2: COVID-19の症状について',
    'No.3: COVID19の症状について',
    'No.4: COVIDの症状について',
    'No.5: <a href="https://www.nicovideo.jp/watch/sm9">再ブレイク!レッツゴー陰陽師</a>',
    );

for my $entry (@entries) {
    if ( $entry =~ /\bCOVID/i ) {
        print "WARNING!: $entry\n";
    }
}

# 実行結果
# WARNING!: No.1: covid-19の症状について
# WARNING!: No.2: COVID-19の症状について
# WARNING!: No.3: COVID19の症状について
# WARNING!: No.4: COVIDの症状について

無事、niconico動画のURLについてはマッチしなくなりました。

今回、COVIDの前にだけ \b を置いたのは、COVIDの後に「-(ハイフン)数字」や「数字」が続くパターンがあり、文字の境界である \w, \W の判別を行なっていないためです。

  • ハイフンが0個以上ある
  • 数字が0個以上ある

という条件を追加することで、より正確なにマッチさせることが可能になります。(マッチしたものは使わないので?:を使っています)

$entry =~ /\bCOVID(?:(-)?\d+)?\b/i

というわけで

Perl入学式 in東京 第5回は延期となってしまいましたが、こういった小ネタを拾っていきたいと思っています。

後、「これをするにはどうしたら良いか?」とか「これをやりたい」「書いてみたがうまく動かない」みたいなのがある方、Perl入学式Slackに書き込んでいただくと、寄ってたかって回答が来ます。ぜひご利用ください。

うまく動かないコードなどはblogやgistなどに貼り付けていただくと、より質の高い回答が来ると思います。

docs.google.com

元ネタ

noteの深津さんのtweetでした。

Perl入学式 2019 in東京 秋開講 第4回 お疲れ様でした

Perl入学式 2019 in東京 秋開講 第4回

受講された方、サポーターの方、お疲れ様でした。 講師をやったジャージの人です。

講義に利用したスライドはMarkdown形式で公開しています。復習に使ってください。

www.perl-entrance.org

また、復習問題を用意しています。
第1回から第4回の内容で解ける問題となっています。ぜひ挑戦してみてください。
復習問題の解答例もありますので、参考にしてください。

なお、この第4回の復習問題はかなり難しく、骨のある問題となっています!(意訳:解けなくても落ち込むな)

問題の意味がわからない、とか、このような解答例はどうだろう?という方はSlackのPerl入学式チャンネル(招待フォーム)や、twitterハッシュタグ #Perl入学式 をつけて聞いてみてください。
応答速度、監視頻度などの面からSlackの方をお勧めします。

会場を提供いただいたadishさん、ありがとうございました。

www.adish.co.jp

講義の途中でちょっとだけ使ったスライド

Perlの初学者向け書籍が「リファレンス」についてどう書いているかを調べてみたものです。

docs.google.com

リファレンス!!

講義中何度も言いましたが、他言語で間接参照(C言語などのポインタ)の概念を習得している方にとっては余裕だったと思います。

しかし、間接参照の概念に初めて触れた方は、理解まで相当時間がかかって大変だと思うんですよね・・・うちがそうでした。

私自身もリファレンスについては習得まで苦労した覚えがあります。

リファレンスについては過去のエントリで大体語り尽くしてる感があるので、そちらもどうぞ。

sironekotoro.hateblo.jp

sironekotoro.hateblo.jp

Perlのリファレンスに関しては、完全な自信をもって深沢千尋さんの「すぐわかる オブジェクト指向 Perl」をお勧めします。

Perl入学式を第4回まで受講してきた方なら、今すぐにでも読み始めて大丈夫なレベル感です。

リファレンスの活用例

以下は2020年2月7日〜2020年2月14日までの日経平均のデータです。平日のみなので5日分です。

このような「日時」に紐づいたデータは色々な場で見ることができると思います。売り上げの日報や、夏休みの宿題にある毎日の気温の記録とか。

info.finance.yahoo.co.jp

日付   始値  高値  安値  終値
2020年2月14日    23,714.52   23,738.42   23,603.48   23,687.59
2020年2月13日    23,849.76   23,908.85   23,784.31   23,827.73
2020年2月12日    23,741.21   23,869.73   23,693.72   23,861.21
2020年2月10日    23,631.79   23,788.25   23,621.72   23,685.98
2020年2月7日     23,899.01   23,943.45   23,759.42   23,827.98

このデータをリファレンスなしで表現するのはとても大変です。変数がいくつも必要になることでしょう。

しかし、リファレンスを使うことで、一つの変数の中にこれらの情報をまとめることが可能になります。

まずはこのように書いてみました。求めたいのは期間中の終値(close)の最大、最小、平均です。講義中に紹介したList::Utilモジュールを使って求めています。

配列の要素にハッシュリファレンスを入れており、そのハッシュリファレンスのkeyは日付、value始値(open), 高値(high), 底値(low), 終値(close)のハッシュリファレンスです。

・・・言葉にすると???なのですが、コードを見た方がわかりやすく思えるはずです。

#!/usr/bin/env perl
use strict;
use warnings;
use List::Util qw/max min sum/;
use Data::Dumper;

my @nikkei225 = (
    {   '2020-02-14' => {
            open  => 23714.52,
            high  => 23738.42,
            low   => 23603.48,
            close => 23687.59
        }
    },
    {   '2020-02-13' => {
            open  => 23849.76,
            high  => 23908.85,
            low   => 23784.31,
            close => 23827.73
        }
    },
    {   '2020-02-12' => {
            open  => 23741.21,
            high  => 23869.73,
            low   => 23693.72,
            close => 23861.21
        }
    },
    {   '2020-02-10' => {
            open  => 23631.79,
            high  => 23788.25,
            low   => 23621.72,
            close => 23685.98
        }
    },
    {   '2020-02-07' => {
            open  => 23899.01,
            high  => 23943.45,
            low   => 23759.42,
            close => 23827.98
        }
    },

);

my @close_values;
for my $row (@nikkei225) {
    my ($date) = keys %{$row};

    # keys は配列を返すが、この例ではkeyは1つの値(日付)だけが必要なので、このようにして受け取る

    push @close_values, $row->{$date}->{close};
}

print "MAX: " . max(@close_values) . "\n";
print "MIN: " . min(@close_values) . "\n";
print "AVG: " . sum(@close_values) / scalar (@close_values) . "\n";

# scalar (@配列) で、配列の要素数を求めることができる

# MAX: 23861.21
# MIN: 23685.98
# AVG: 23778.098

欲しかった結果は得ることができました。

しかし、ここで「欲望」が囁きます。最大値、最小値を記録したのは何月何日なのだ?と。

うーん、これは難しい。そう、このリファレンスのままでは難しい

というわけで、別なデータ構造に書き直してみます。ついでに、最大と最小はList::Utilモジュールを使わずにやってみます。

配列の中にハッシュリファレンスを入れたものです。先ほどのよりもシンプルな構造です。

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

my @nikkei225 = (
    {   date  => '2020-02-14',
        open  => 23714.52,
        high  => 23738.42,
        low   => 23603.48,
        close => 23687.59
    },

    {   date  => '2020-02-13',
        open  => 23849.76,
        high  => 23908.85,
        low   => 23784.31,
        close => 23827.73
    },
    {   date  => '2020-02-12',
        open  => 23741.21,
        high  => 23869.73,
        low   => 23693.72,
        close => 23861.21
    },
    {   date  => '2020-02-10',
        open  => 23631.79,
        high  => 23788.25,
        low   => 23621.72,
        close => 23685.98
    },
    {   date  => '2020-02-07',
        open  => 23899.01,
        high  => 23943.45,
        low   => 23759.42,
        close => 23827.98
    },
);

my $max = 0;     # 最大値を記録するための変数
my $max_date;    # 最大値の日付を記録するための変数
my $min = 38915
    ; # 最少値を記録するための変数(初期値は1989年12月29日の日経平均 史上最高値)
my $min_date;    # 最少値の日付を記録するための変数
my $sum = 0;
for my $row (@nikkei225) {
    my $close = $row->{close};

# $maxよりも大きい終値($row->{close})だったら変数の値と日付を更新
    if ( $max < $close ) {
        $max      = $close;
        $max_date = $row->{date};
    }

# $minよりも小さい終値($row->{close})だったら変数の値と日付を更新
    if ( $close < $min ) {
        $min      = $close;
        $min_date = $row->{date};
    }

    # 平均を求めるために、終値の合計額を集計しておく
    $sum += $close;
}

print "MAX: $max_date : " . $max . "\n";
print "MIN: $min_date : " . $min . "\n";
print "AVG: " . $sum / scalar(@nikkei225) . "\n";

# scalar (@配列) で、配列の要素数を求めることができる

# MAX: 2020-02-12 : 23861.21
# MIN: 2020-02-10 : 23685.98
# AVG: 23778.098

これで「欲望」の求めることを達成することができました。

このように、リファレンスを習得することで、「欲しいもの」を得やすいデータ構造を作り出すことができるようになります。

・・・ところで、第2回で学んだsortを覚えていますか?配列の要素を並べ替えするための関数です。

終値を順に並べれば、もっと楽に最大値、最小値を求めることができるのでは・・・?

リファレンスのソートはなかなかに難易度が高いです。興味のある人は是非Googleで検索しつつ実装してみてください。私も書いてみました。

最大値、最小値を求めるときにsortを使ってみる(クリックで展開)

#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;

my @nikkei225 = (
    {   date  => '2020-02-14',
        open  => 23714.52,
        high  => 23738.42,
        low   => 23603.48,
        close => 23687.59
    },

    {   date  => '2020-02-13',
        open  => 23849.76,
        high  => 23908.85,
        low   => 23784.31,
        close => 23827.73
    },
    {   date  => '2020-02-12',
        open  => 23741.21,
        high  => 23869.73,
        low   => 23693.72,
        close => 23861.21
    },
    {   date  => '2020-02-10',
        open  => 23631.79,
        high  => 23788.25,
        low   => 23621.72,
        close => 23685.98
    },
    {   date  => '2020-02-07',
        open  => 23899.01,
        high  => 23943.45,
        low   => 23759.42,
        close => 23827.98
    },
);

my $sum = 0;
for my $row (@nikkei225) {
    my $close = $row->{close};

    # 平均を求めるために、終値の合計額を集計しておく
    $sum += $close;
}

my @sorted = sort { $a->{close} <=> $b->{close} } @nikkei225;

# 配列の最初の要素を求めるときの添え字は[0]
# 配列の最後の要素を求めるときの添え字は[-1]

print "MAX: $sorted[-1]->{date} : " . $sorted[-1]->{close} . "\n";
print "MIN: $sorted[0]->{date} : " . $sorted[0]->{close} . "\n";
print "AVG: " . $sum / scalar(@nikkei225) . "\n";

# scalar (@配列) で、配列の要素数を求めることができる

# MAX: 2020-02-12 : 23861.21
# MIN: 2020-02-10 : 23685.98
# AVG: 23778.098

Perl入学式 第5回 Webアプリ編

次回のPerl入学式 in東京は秋季講習の最終回、第5回 Webアプリ編です。

サブルーチンとWebアプリの基礎をやります。connpassでページを公開しています。

ただし、新型コロナウイルスの蔓延状況次第では延期の可能性があります。ご了承ください。【2月18日追記】残念ながら、第5回は延期となりました。

perl-entrance-tokyo.connpass.com

また、Webアプリを作成していくにあたり、HTMLの最低限の知識が必要となります。もし不安な方は一度下記のテキスト「HTML入学式」を見ておくことをお勧めします。

github.com

Perl入学式 第3回までの範囲(+α)でROT13

というわけで、前回からの続きです。

sironekotoro.hateblo.jp

Perl入学式 第3回ではハッシュと正規表現を扱いました。

ハッシュを使ってROT13を解く

ROT13はハッシュと正規表現の文字クラスを利用することで簡単に解くことが可能です。

まずはハッシュで解いてみます。ちょっと長くなりますが、密度は薄いです。

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

my $secret_str = 'uryyb jbeyq';    # 暗号化済みの文字列
my @secret_str = split "",
    $secret_str;    # 暗号化文字列を1文字ずつ配列に格納

# 一文字ずつ対応したハッシュ
my %hash = (
    a => 'n',
    b => 'o',
    c => 'p',
    d => 'q',
    e => 'r',
    f => 's',
    g => 't',
    h => 'u',
    i => 'v',
    j => 'w',
    k => 'x',
    l => 'y',
    m => 'z',
    n => 'a',
    o => 'b',
    p => 'c',
    q => 'd',
    r => 'e',
    s => 'f',
    t => 'g',
    u => 'h',
    v => 'i',
    w => 'j',
    x => 'k',
    y => 'l',
    z => 'm',
);

for my $char (@secret_str) {
    if ( $char eq ' ' )
    {    # 文字ではなく、スペースだったらそのまま表示
        print $char;
    }
    else {
        print $hash{$char};
    }
}

正規表現の文字クラスを使ってROT13を解く

現在のカリキュラムでは、正規表現を使った置換を解説しています。 s///gですね。

ただ、今回のように置換前と置換後の文字列が1文字ずつ対応する場合には、tr/// がベストマッチです。

例えばこんな感じです。tr の前半にある置換対象の数字が、置換後の文字に置き換わっています。

my $str = '123';
$str =~ tr/123/abc/;

# 1 と a が対応する
# 2 と b が対応する
# 3と c が対応する

print $str; # abc

あくまで1文字ずつ対応していれば良いので、このような変換も可能です。

my $str = '123';
$str =~ tr/123/cba/;

print $str; # cba

置換前の文字列と、置換後の文字列には、文字クラスを利用することが可能です。最初の例を文字クラスを使って書いてみるとこんな感じです。

2020年2月1日追記

tr/SEARCHLIST/REPLACEMENTLIST/ のSEARCHLISTに該当するところには文字クラスに[ ] は不要です。ということで掲載したサンプルコードを編集しています。本エントリのxtetsujiさんのコメントに感謝。

my $str = '123';
$str =~ tr/1-3/a-c/;

print $str; # abc

置換対象に含まれていない文字についてはそのままです。以下の例だと、4から9までの数字です。

my $str = '123456789';
$str =~ tr/1-3/a-c/;

print $str; # abc456789

さて、これを踏まえてROT13を解いてみるとこんな感じです。

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

my $secret_str = 'uryyb jbeyq';    # 暗号化済みの文字列
$secret_str =~ tr/a-z/n-za-m/;
print $secret_str;  # hello world

実質3行で終わってしまいました。

文字クラスは頻出の a-z だけではなく、 n-za-m のような利用も可能です。

a-z はアルファベット順の abcdefghijklmnopqrstuvwxyz です。

n-za-mnopqrstuvwxyzabcdefghijklm と同じです。

PerlでROT13を解いてみて

ということで、Perl入学式の範囲でROT13を解いてみました。

文字列を置換する、という本来の目的に沿った関数を使うことでスクリプトが簡潔になります。

for文で1文字ずつバラして書くのも「頭の対応」「縛りプレイ」感があって楽しいのですが、できれば楽したいっすね。

なんか遠回りしてるな・・・?と思ったら、目的に沿った関数やモジュールが公開されているかもしれません。

実現したい方法を探そうにも、それがぼんやりしていて・・・という人は是非Perl入学式のSlackや懇親会で相談してみてください。うちもよく相談してます。

(余談)なんでROT13?

なぜROT13でエントリを2つ書いたのか?というと、現在勉強中のGo言語で同じ問題が出たからです。

あ、ちなみにGo言語の公式チュートリアルである Tour of Go は途中で理解できなくなった勢なので、わかるところまでレベル下げた本がこれって感じです。まだ半分くらいだけど、うちにとっては、とても分かりやすくて良い本です。

Go言語の勉強・・・に限らずですが、練習問題に出会うと「この問題はPerlではどう解くのだろう?」となってしまい、2倍くらい時間がかかってる感じですね・・・

まずGoで自力で解いてみる、解答を写経してみる、次にGo言語っぽくPerlで書いて、次にめっちゃPerlっぽく書く。時間がかかるんだけど楽しいのでやめられないですよね。

(目的と手段がすり替わるいつものパターン)

package main

import "fmt"

func main() {
    message := "uryyb jbeyq"

    for i := 0; i < len(message); i++ {

        c := message[i]
        if 'a' <= c && c <= 'z' {
            c += 13

            switch {
            case c > 'z':
                c = c - 26
            }
        }
        fmt.Printf("%c", c)
    }

}

何と、Go言語では文字に数字足したり引いたりできるんですよすごい。こんな感じ。

package main

import "fmt"

func main() {
    char := 'c'

    fmt.Printf("%c\n", char) // c
    char = char + 1
    fmt.Printf("%c\n", char) // d
}

ついでに、Go言語で書いたFizzBuzzです。Perlしか知らなくても、何となく読めませんかね? for とか if とか剰余の % とか。

package main

import "fmt"

func main() {

    for i := 1; i <= 100; i++ {
        if i%3 == 0 && i%5 == 0 {
            fmt.Println("fuzzbuzz")
        } else if i%3 == 0 {
            fmt.Println("fizz")
        } else if i%5 == 0 {
            fmt.Println("buzz")
        } else {
            fmt.Println(i)
        }
    }
}

うちは産湯を使った言語がPerlなので、型がある言語をちゃんと勉強するのは初めてです。

でも、Perlで学んだ処理の流れ+ Go ならではの知識・やり方を知るのが楽しいですし、それによって自分のPerlコードも変わっていく、というのがまた楽しいです。

「わかることは変わること」とよく言われますが、本当にそういう感じです。

いろんな言語の良いところを知って、それを取り込み、Perl力を高めていきたいですね。

Perl入学式 2019 in東京 秋開講 第3回 お疲れ様でした

Perl入学式 2019 in東京 秋開講 第3回

受講された方、サポーターの方、お疲れ様でした。 講師をやったジャージの人です。

講義に利用したスライドはMarkdown形式で公開しています。復習に使ってください。

www.perl-entrance.org

また、復習問題を用意しています。
第1回から第3回の内容で解ける問題となっています。ぜひ挑戦してみてください。
復習問題の解答例もありますので、参考にしてください。

問題の意味がわからない、とか、このような解答例はどうだろう?という方はSlackのPerl入学式チャンネル(招待フォーム)や、twitterハッシュタグ #Perl入学式 をつけて聞いてみてください。
応答速度、監視頻度などの面からSlackの方をお勧めします。

講義の途中でちょっとだけ使ったスライド

正規表現の量演算子., ?, +, * )について解説したスライドです。参考にどうぞ。最後の2枚には3月27日、28日に開催される YAPC::Kyoto 2020 について紹介しています。ホテル確保はお早めに!!

docs.google.com

ハッシュ

今回の第3回の前半ではハッシュを学びました。

Perlにおける基本的な変数は以下の3つです。今回で全てを学習したことになります。

  • $ から始まるスカラー変数
  • @ から始まる配列
  • % から始まるハッシュ

次回学習するリファレンスはこの3つの変数を利用する応用編ともいえます。

正規表現

正規表現により、単なる検索、置換以上のことが可能になります。

VimVSCodeなどのエディタでは検索条件として「検索文字そのもの」だけではなく、正規表現で検索が可能です。

Everything のようなソフトウェアを利用することで、PC内のファイル検索に正規表現が利用できます。

富士通_2020_0190.xlsx 富士ゼロックス_2019_0820.xlsx

などのファイルは富士.+_\d{4}_\d{4}\.xlsxという形で検索できますし、もっとざっくり 富士.*\.xlsxでも引っかけることができますね。

WebサーバとしてメジャーなApachenginxにおいても、URLの書き換え等に正規表現を用る事ができます。

もちろん、Perl以外のプログラム言語でも正規表現は利用できます。今回学習した「マッチング」「後方参照」「最長マッチ・最短マッチ」等の概念は他でも応用可能なものです。

このように活用の機会がそこかしこにあるのが正規表現です。

全てを理解するのは大変ですが、わからない時に講義資料を見にきてくれれば良いなぁと思ってます。

また、現Perl入学式校長である id:xtetsuji さんが昨年、Web+DB Press正規表現についての記事を書いています。

今回の講義で物足りなかった方、もっと先に進みたい方、正規表現の深淵を覗いてみたい方は是非ご覧ください。

gihyo.jp

次回のPerl入学式 in東京 は2月15日

次回はリファレンスを学びます。

これまでに学んだ3種の変数をベースにより複雑なデータ構造を作り、利用することが可能になります。

リファレンス、それは初学者に立ちはだかる壁・・・実際うちもPerl入学式1周目でわからず、本を読んで薄ぼんやり、Perl入学式2周目でなんとか、あとは欲望のままに欲望を叶えるスクリプトを書いているうちに身に付けました。

つまり数です。

脳筋理論で申し訳ないのですが、数をこなせば乗り越えられる壁なので、2度3度分からない程度でくじけずやっていきましょう!

perl-entrance-tokyo.connpass.com

Perl入学式 第2回までの範囲(+α)でROT13

ROT13 というのは、簡単な暗号の一つです。

暗号化した文字列 uryyb jbeyq を以下の表をもとに置換すると、hello world という文字列になります。

変換前 a b c d e f g h i j k l m
変換後 n o p q r s t u v w x y z
変換前 n o p q r s t u v w x y z
変換後 a b c d e f g h i j k l m

名前の通り、 a は 13文字後の n に置換されており、他の文字も同様です。13文字後が z 以降になる場合には、a に戻ります。

ja.wikipedia.org

さて、これを Perl入学式の第2回までの範囲でやってみましょう!

Perl入学式の第2回は四則演算、配列、配列操作の関数、forを使った繰り返しまで、でした。

github.com

書いてみたものがこちらです。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

my $secret_str = 'uryyb jbeyq';          # 暗号化済みの文字列
my @secret_str = split "", $secret_str;  # 暗号化文字列を1文字ずつ配列に格納

my @alphabet = ( 'a' .. 'z' );    # アルファベットが1文字ずつ格納された配列

# 暗号化した文字列を1文字ずつ処理する。
for my $char (@secret_str) {

    if ( $char eq ' ' )
    {    # 文字ではなく、スペースだったらそのまま表示
        print $char;
    }
    else {

        my $i     = 0;  # ループが何回目かを保存する変数
        my $index = 0;  # アルファベットの何文字目かを保存する変数
                        # @alphabetの添え字と同じ

        for my $c (@alphabet) {     #アルファベットを1文字ずつ取り出して比較する

            if ( $char eq $c ) {    # 文字列なので比較は eq
                $index = $i;        # 合致したら $indexに何番目のアルファベットだったか保存
            }
            else {
                $i++;               # 合致しなかったら、$indexに1加える
            }
        }

        my $recover_index = $index + 13;   # 今回の暗号は13文字ずらしたことがわかっているので、
                                                                  # 13文字進めたものを本来の文字列の添字とする

        if ( $recover_index > 26 ) {    # 13文字分添字を足したら、アルファベットの文字数を超えた場合

            print $alphabet[ $recover_index % 26 ]; # 剰余で26を超えた分だけ添字にする

        }
        else {
            print $alphabet[$recover_index];        # 添字をそのまま使う
        }
    }
}

ううむ、書けなくはないのですが、ちょっと長いですね・・・でも書けないことはないです!

lastでforをぬけてみる。

さて、ここから第2回の範囲を超えていくとどうなるのか?というのをやっていきます。

for 文や while 文などの繰り返しの中で、途中で抜けたいときに使う last を使います。ちょっとだけ短くなりました。変数も1つ($i)消えました。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

my $secret_str = 'uryyb jbeyq';
my @secret_str = split "", $secret_str;

my @alphabet = ( 'a' .. 'z' );

for my $char (@secret_str) {

    if ( $char eq ' ' )
    {
        print $char;
    }
    else {

        my $index = 0;

        for my $c (@alphabet) {

            if ( $char eq $c ) {
                last;               # 文字が合致した場合には、そこで抜ける
            }
            else {
                $index++;
            }
        }

        my $recover_index = $index + 13;

        if ( $recover_index > 26 ) {

            print $alphabet[ $recover_index % 26 ];

        }
        else {
            print $alphabet[$recover_index];
        }
    }
}

index関数で、何文字目かを文字列から取ってみる

さらに超えていきます。Perlindex 関数は以下のように用います。文字列から、特定の文字が何番目にあるかを返す関数です。

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

my $alphabet = 'abcdefghijklmnopqrstuvwxyz';

my $index = index $alphabet, 'a';
print $index . "\n";    # 0

$index = index $alphabet, 'n';
print $index . "\n";    # 13

$index = index $alphabet, 'z';
print $index . "\n";    # 25

この index関数があれば、アルファベットの添え字が何番目かを調べるのにforを使う必要がなくなりますね!

for文 の中にある for文(入れ子のfor文) が消えてすっきりしました。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

my $secret_str = 'uryyb jbeyq';
my @secret_str = split "", $secret_str;

my @alphabet = ( 'a' .. 'z' );

# index関数を使うため、アルファベットを格納したスカラー変数を用意
my $alphabet_str = join "", @alphabet;

for my $char (@secret_str) {

    if ( $char eq ' ' ) {
        print $char;
    }
    else {

        my $index = index $alphabet_str, $char;  # index関数で何文字目かを調べる

        my $recover_index = $index + 13;

        if ( $recover_index > 26 ) {

            print $alphabet[ $recover_index % 26 ];

        }
        else {
            print $alphabet[$recover_index];
        }
    }
}

三項演算子を使う

まだ、短くすることはできるのでしょうか?もちろん可能です。ただ、「第2回までの範囲をそれほど逸脱しない」となると難しいですね・・・次で最後とします。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

my $secret_str = 'uryyb jbeyq';
my @secret_str = split "", $secret_str;

my @alphabet = ( 'a' .. 'z' );

# index関数を使うため、アルファベットを格納したスカラー変数を用意
my $alphabet_str = join "", @alphabet;

for my $char (@secret_str) {

    if ( $char eq ' ' ) {
        print $char;
    }
    else {

        my $index = index $alphabet_str,
            $char;    # index関数で何文字目かを調べる

        my $recover_index = $index + 13;

        # 三項演算子
        $recover_index
            = $recover_index > 26 ? $recover_index % 26 : $recover_index;

        print $alphabet[$recover_index];

    }
}

添字が26より大きかった場合を処理していたif文が消えています。

これは三項演算子を用いた条件分岐の書き方です。三項演算子Perlだけではなく、C言語Java, PHPにもあります。

ja.wikipedia.org

Perl入学式の第2回までの範囲から、last, index, 三項演算子を用い他スクリプトを書いてみました。

同じ結果をもたらすスクリプトでも、行数や変数の数が大きく変わってきます。

Perl入学式の範囲から越境して、いろいろな関数を触ってみてください!

perldoc.jp

Perl入学式第3回までの範囲だと・・・?

2020年01月25日はPerl入学式 in東京の第3回です。

perl-entrance-tokyo.connpass.com

ハッシュと正規表現を使うことで、ROT13は更にわかりやすくなり、更に短くすることも可能です。

(多分)講義では取り上げませんが、このブログでは第3回の範囲でROT13を書いてみたいと思っています。

Perl入学式 in 東京 秋開講 第2回 ピザ会でのお題「コラッツの問題」

Perl入学式 in 東京では各回の講義終了後にピザ会(ピザ&ジュース代は参加者負担)を開催しており、そこでサポーター・受講者さんと雑談などをしております。

サポーター含め参加者が抱えているプログラムの問題や、詰まってしまったところを相談したり、エディタやGitの使い方、PerlExcelなど他のツール・ソフトの連携方法、業界の動向について話したりしています。全体的にゆるーい雰囲気です。

その中で、id:xtetsuji さんが課題が出し、志願者がコードを書いてその場で発表、という試みを行っています。

今回のお題:コラッツの問題

xtetsujiさんからのお題ですが、今回は「コラッツの問題」でした。コラッツ・・・聞いたことないですね・・・コラッタの仲間かな?来年ねずみ年だし・・・

コラッツの問題 - Wikipedia

コラッツの問題は、「任意の正の整数 n をとり、

n が偶数の場合、n を 2 で割る
n が奇数の場合、n に 3 をかけて 1 を足す

という操作を繰り返すと、どうなるか」というものである。

ねずみ関係なかった。

このコラッツの問題を 1, 2, 3, 4, 5 ・・・ と与える数字を変えていき、どのような経緯で数値が変わっていくかを出して欲しい、というものでした。

コラッツの問題を解くには・・・?

この問題は今回学習した、「繰り返し」と「条件分岐」に「四則演算」で解くことができますが、それだけではちょっと足りません。

例えば、n = 5 とすると以下のような経緯を経て1になります。

5: 16 -> 8 -> 4 -> 2 -> 1 -> end

n = 7 の場合にはこう。

7: 22 -> 11 -> 34 -> 17 -> 52 -> 26 -> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end

つまり、与える数によって繰り返しの数が異なります。繰り返しの数が読めない場合、ちょっとした工夫が必要です。

それは、特定の条件の時に繰り返しを終わらせる last です。

繰り返し を抜ける last

これは Hello world! を10回表示するスクリプトです。

for ( 1 .. 10 ) {
    print "Hello world!\n";
}

これに1行加えます。

for ( 1 .. 10 ) {
    print "Hello world!\n";
    last;
}

このようにすると、Hello world!は1回のみ表示されます。処理が last に到達すると、その繰り返しから抜けると動きとなります。

コラッツの問題、何回ループするかは分からないので、とりあえず10000回くらいにしておきます。

lastを使う条件は n = 1 になった時です。

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

print "input number > ";

my $input_num = <STDIN>;
chomp $input_num;

print "$input_num: ";

for ( 0 .. 10000 ) {

    if ( $input_num == 1 ) {
        print "end\n";
        last;
    }
    elsif ( $input_num % 2 == 0 ) {

        # n が偶数の場合、n を 2 で割る
        $input_num = $input_num / 2;
    }
    else {
        # n が奇数の場合、n に 3 をかけて 1 を足す
        $input_num = $input_num * 3 + 1;
    }

    print $input_num . " -> ";

}

last のようにループ内で使える関数は nextredo があります。

perldoc.jp

回数を指定しないループ、無限ループ

先ほどの解き方だと、10000回の繰り返しで終わらなかった時には途中で終わってしまいます。

このように、繰り返しの回数が分からない場合には while を使います。Perl入学式のテキストでは「落ち穂拾い」として掲載しています。

落ち穂拾い: while ループ

while を使う時には無限ループにならないよう、ループを終わらせる処理を入れることが必要です。last ですね。

もし、処理を書き間違えて無限ループになってしまったら、Ctrl(Control)キーを押しながらCを押すことで強制的に止めることが可能です。

コラッツの問題のwhile版です。

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

print "input number > ";

my $input_num = <STDIN>;
chomp $input_num;

print "$input_num: ";

while (1) {

    if ( $input_num == 1 ) {
        print "end\n";
        last;
    }
    elsif ( $input_num % 2 == 0 ) {
        # n が偶数の場合、n を 2 で割る
        $input_num = $input_num / 2;
    }
    else {
        # n が奇数の場合、n に 3 をかけて 1 を足す
        $input_num = $input_num * 3 + 1;
    }

    print $input_num . " -> ";

}

繰り返しを繰り返す

さて、当初のレギュレーションでは与える数が変わっていく・・・というものでした。

今回はループの中にループを作る「二重ループ」で実現してみました。このような二重ループは「入れ子いれこ構造」「ネスト構造」などとも呼ばれます。

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

for my $number ( 1 .. 10 ) {

    print "$number: ";

    while (1) {

        if ( $number == 1 ) {
            print "end\n";
            last;
        }
        elsif ( $number % 2 == 0 ) {

            # n が偶数の場合、n を 2 で割る
            $number = $number / 2;
        }
        else {
            # n が奇数の場合、n に 3 をかけて 1 を足す
            $number = $number * 3 + 1;
        }

        print $number . " -> ";

    }

}
1: end
2: 1 -> end
3: 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end
4: 2 -> 1 -> end
5: 16 -> 8 -> 4 -> 2 -> 1 -> end
6: 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end
7: 22 -> 11 -> 34 -> 17 -> 52 -> 26 -> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end
8: 4 -> 2 -> 1 -> end
9: 28 -> 14 -> 7 -> 22 -> 11 -> 34 -> 17 -> 52 -> 26 -> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end
10: 5 -> 16 -> 8 -> 4 -> 2 -> 1 -> end

二重ループについては、掛け算の「九九の表」をスクリプトで再現してみる、ってのも良いかもしれません。

Perl入学式の第2回までの範囲で書いてみるとこんな感じですかね。

スペースの扱いをもっとスマートにしたい!という人は printf 関数に挑戦してみると良いと思います。

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

print '    1  2  3  4  5  6  7  8  9' . "\n";

for my $tate (1 .. 9){
    print "$tate:";
    for my $yoko ( 1 .. 9){

        if ($tate * $yoko < 10){
            print '  ' . $tate * $yoko;
        }else{
            print  ' ' . $tate * $yoko;
        }
    }
    print "\n";
}
    1  2  3  4  5  6  7  8  9
1:  1  2  3  4  5  6  7  8  9
2:  2  4  6  8 10 12 14 16 18
3:  3  6  9 12 15 18 21 24 27
4:  4  8 12 16 20 24 28 32 36
5:  5 10 15 20 25 30 35 40 45
6:  6 12 18 24 30 36 42 48 54
7:  7 14 21 28 35 42 49 56 63
8:  8 16 24 32 40 48 56 64 72
9:  9 18 27 36 45 54 63 72 81

Perl入学式 2019 in東京 秋開講 第2回 お疲れ様でした

Perl入学式 2019 in東京 秋開講 第2回

受講された方、サポーターの方、お疲れ様でした。 講師をやったジャージの人です。

講義に利用したスライドはMarkdown形式で公開しています。復習に使ってください。

www.perl-entrance.org

また、復習問題を用意しています。
第1回と第2回でやった内容のみで解ける問題となっています。ぜひ挑戦してみてください。
復習問題の解答例もありますので、参考にしてください。

問題の意味がわからない、とか、このような解答例はどうだろう?という方はSlackのPerl入学式チャンネル(招待フォーム)や、twitterハッシュタグ #Perl入学式 をつけて聞いてみてください。
応答速度、監視頻度などの面からSlackの方をお勧めします。

講義の途中でちょっとだけ使ったスライド

わかりやすい変数名をつけて、3ヶ月後の自分を救いましょう!

docs.google.com

FizzBuzzの素晴らしさ

講義中も何度か言いましたが、FizzBuzz問題は「繰り返し」「条件分岐」というプログラムの基礎を内包した素晴らしい問題です。

是非、次回の講習までにFizzBuzzをマスターしてしまいましょう!

FizzBuzzや配列については、このブログの昨年の記事も参考にどうぞ。

sironekotoro.hateblo.jp

ピザ会のお題

ちょっと長くなったので別エントリに起こします。起こしました

sironekotoro.hateblo.jp

次回のPerl入学式 in東京は1月25日!

既にconnpassに掲載しています。皆様の参加をお待ちしています!

perl-entrance-tokyo.connpass.com

おまけ