sironekotoroさんという方が、拙著『かんたんPerl』(以降本書と呼びます)をご購入の上、学習日記を公開してくださっています。ありがとうございます。
 で、その中の記事で、第3章の練習問題2が間違っているのではないだろうか。それも問題文に誤りがあるのではないか、というご指摘がありました。

かんたんPerl 第2章〜第3章 - sironekotoroの日記

 内容を確認したところ、たしかに本が間違っていたので、sironekotoroさんにご連絡し、ブログの紹介をご快諾いただきましたので、ここにご紹介すると共に、本書の内容を訂正してお詫びします。

Pearl-variety hg

訂正とお詫び

 問題の問題はp.94のQ2です。
 問題文は以下のようです。
Q2
 次のプログラムの穴埋めをして、月の英語名から数字を返すプログラムを作ってください。
#! /usr/bin/perl
#
# monthName.pl -- 月の名前から順番を割り出そう

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12";

my $month = "March"; # 他の月を調べたいときはここを変更する

>
>   ?
>

say "英語で$monthは$num月のことです。";
 結果は以下のようになります。
C:\Perl\perl> monthName.pl
英語でMarchは3月のことです。

 この問題文の変数初期化を、以下のように訂正します。

(誤)
my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12";

(正)
my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12 ";

 どこが変わったかというと、超微妙ですが、二重引用符文字列の末尾の「December 12"」の12と"の間に空白を入れるのが正解です。
 これで問題なく進めていただけると思います。
 どうも申し訳ありませんでした。

解説

 さて、以降は自分の間違いについて偉そうに解説を加えます。
 上のプログラム、穴埋め後の模範解答は以下のようになっていました。

#! /usr/bin/perl
#
# monthName.pl -- 月の名前から順番を割り出そう

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12";

my $month = "March"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num_e = index($year, " ", $num_s) - 1;
 # さらに次に出てくる空白のオフセットを探す。
 # その前($num_e)までが月の順番である
my $len = $num_e - $num_s + 1;
 # 月の順番の長さ(9月までは1、10月以降は2)を得る
my $num = substr $year, $num_s, $len;
 # 月の順番を得る
say "英語で$monthは$num月のことです。";

 プログラム内にリテラルで「March」と月数を調べたい月の英語名を埋め込んでいるのですが、Marchについては確かに正しく動きます。

[sample]$ ./monthName.pl
英語でMarchは3月のことです。

 ところが、これを「December」を調べるようにするとうまくいきません。

#! /usr/bin/perl
#
# monthName2.pl -- 月の名前から順番を割り出そう
#(12月版)(問題あり!)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12";

my $month = "December"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num_e = index($year, " ", $num_s) - 1;
 # さらに次に出てくる空白のオフセットを探す。
 # その前($num_e)までが月の順番である
my $len = $num_e - $num_s + 1;
 # 月の順番の長さ(9月までは1、10月以降は2)を得る
my $num = substr $year, $num_s, $len;
 # 月の順番を得る
say "英語で$monthは$num月のことです。";

 実行してみます。

[sample]$ ./monthName2.pl
英語でDecemberは月のことです。
[sample]$

 ダメですね。

 原理を説明すると、このプログラムでは月名と月数が"January 1 February 2 March 3 April…"のように空白を挟んで続いているので、まず月の英語名の次の空白を探して変数$num_sに記憶し、さらにその次の空白を$num_eに記憶する。その間の文字列を取得すれば、それが求める月の数字である、という前提に依って立っています。
 しかし、最後のDecemberだけは、"…November 11 December 12"という風に、数字の後ろに空白がなく終わってしまいました。
 よって、$num_eはindex関数によって-1を返すので、月数の長さ$lenはマイナスの値になります。substr関数の第3引数(取得する文字列の長さ)としてマイナスの数を与えた場合、空文字列が返り、結果は「Decemberは月のことです」などという、ちょっと詩的な間違いになってしまいました。
 これは、上で述べた通り、問題文の"…December 12"の末尾に空白を足してやれば済みます。

#! /usr/bin/perl
#
# monthName3.pl -- 月の名前から順番を割り出そう
#(12月版)(問題修整ズミ!)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12 "; # 末尾に空白を追加

my $month = "December"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num_e = index($year, " ", $num_s) - 1;
 # さらに次に出てくる空白のオフセットを探す。
 # その前($num_e)までが月の順番である
my $len = $num_e - $num_s + 1;
 # 月の順番の長さ(9月までは1、10月以降は2)を得る
my $num = substr $year, $num_s, $len;
 # 月の順番を得る
say "英語で$monthは$num月のことです。";

 実行してみます。

[sample]$ ./monthName3.pl
英語でDecemberは12月のことです。
[sample]$

現状のままなんとか解けないか?

 さて、上の問題は、問題文に穴埋めが必要なプログラムがあったのだが、その穴埋め以外の部分に問題文時点であらかじめ間違いがあったために、模範解答の穴埋めを埋めたプログラムがうまく動かないという問題でした。
 では、問題文に間違いがある状態で、なんとか無理矢理動くプログラムを作れないでしょうか。
 なんと、sironekotoroさんは天才的にも、この解決に成功してしまいました。ということで、それをここでも紹介しようと思います。

 余談ですが、『かんたんPerl』を執筆するに当たって、各章の末尾に練習問題を付けることは、編集者の方の要望で決まりました。学校の教科書として使うときに、練習問題があった方が便利だということです。なるほど! とぼくは賛同して、いろいろ練習問題を作ったのですが、非常に苦しみました。特に、最初の方の問題を作るのに苦しみました。第1章では第1章までの、第2章では第2章までのプログラミングの知識しか使えないのです。よって、わずかなプログラムの機能で、しかもある程度面白い問題を作らなければなりません。sironekotoroさんも理解していただいていますが、第3章では、split関数も、配列も、ハッシュも、ループ構造も、正規表現も使えず、ただindex関数とsubstr関数を使って問題を解かなければならないというわけです。

 じっさい、ぼくはあきらめて、sironekotoroさんのブログから回答をいただきました。なるほどー!と思いました。
  • 1〜9月の場合は「月名 n 」のように、月名の後ろに空白1文字、数字1文字、空白1文字が来る
  • 10〜12月の場合は「月名 nn」のように、月名の後ろに空白1文字、数字2文字が来る
  • よって、月名の後ろ3文字の中に、必ず月数が入っている
 なるほどー! ということで、ここではsironekotoroさんの考え方だけいただいて、いちおうぼくのオリジナルのプログラムを書いてみます。

#! /usr/bin/perl
#
# monthName4.pl -- 月の名前から順番を割り出そう
# (問題を変えずになんとか解く!)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12"; # また空白を取り除く

my $month = "December"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num = substr $year, $num_s, 2;
 # $num_sから2バイトが必ず月数を含む

say "英語で$monthは$num月のことです。";

 ではまず、12月で実行してみます。

[sample]$ ./monthName3.pl
英語でDecemberは12月のことです。
[sample]$

 いいですね。次にプログラムの中のリテラルを変更して、Marchに戻して実行してみます。

#! /usr/bin/perl
#
# monthName5.pl -- 月の名前から順番を割り出そう
# (問題を変えずになんとか解く!)(March編)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12"; # また空白を取り除く

my $month = "March"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num = substr $year, $num_s, 2;
 # $num_sから2バイトが必ず月数を含む

say "英語で$monthは$num月のことです。";

 では実行。

[sample]$ ./monthName5.pl
英語でMarchは3 月のことです。

 予定の行動ですが、3の後ろに空白が1文字入っています。
 これを取り除くにはどうすればいいのでしょうか。
 ここでも、sironekotoroさんの天才的な思いつきを借ります。
  • $xに"3 "のような空白と数字からなる文字列が入っている場合、ゼロを足すと、数字コンテキストで解釈されるので、"3"になる
 へぇー! なるほど! たしかにこれも第2章で出てきた知識ですね。ではやってみます。

#! /usr/bin/perl
#
# monthName6.pl -- 月の名前から順番を割り出そう
# (問題を変えずになんとか解く!改)(March編)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12"; # また空白を取り除く

my $month = "March"; # 他の月を調べたいときはここを変更する

my $name = index($year, $month);
 # これで$monthのオフセットが得られる
my $num_s = index($year, " ", $name) + 1;
 # $monthの次に出てくる空白のオフセットを探す。
 # その次($num_s)から月の順番が始まる
my $num = substr $year, $num_s, 2;
 # $num_sから2バイトが必ず月数を含む
$num += 0;
 # ゼロを加算して数値化する


say "英語で$monthは$num月のことです。";

 さあどうだ。

[sample]$ ./monthName6.pl
英語でMarchは3月のことです。

 見事に解けましたね! 念のため、12月に戻しても正しく動作します。

もしも縛りがなかったら?

 sironekotoroさんの考察はさらに進みます。「もし、第3章までのPerlの知識を使うという縛りがなく、どんなコードでも書いていいという前提であれば、どうやれば問題は解けるか?」面白い! sironekotoroさんは読者の鑑ですね。ということで、ぼくももし縛りがなければ、どんなコードになるのか書いてみます。

#! /usr/bin/perl
#
# monthName7.pl -- 月の名前から順番を割り出そう
# (問題を変えずに、あらゆるPerlの知識を使って解く!)(March編)

use 5.010;
use strict;
use warnings;

my $year = "January 1 February 2 March 3 April 4 May 5 June 6 July 7 August 8 September 9 October 10 November 11 December 12"; # また空白を取り除く

my $month = "March"; # 他の月を調べたいときはここを変更する

my $num;
my %year = split / /, $year;
$num = $year{$month};

say "英語で$monthは$num月のことです。";

 どうでしょう。

[sample]$ ./monthName7.pl
英語でMarchは3月のことです。

 できてるっぽいですね。

 いちおうプログラムの中身を解説します。

my $num;
my %year = split / /, $year;

 まず、回答を格納する$numをmy宣言します。
 次に、$yearを空白文字でsplitします。この時点で、split関数は奇数要素目(インデックス付けで0、2、4…番目)に月の名前を、偶数要素目(インデックス付けで1、3、5…番目)に月数を持つリストを返します。
 で、ハッシュ%yearをmy宣言し、split関数の戻り値を代入します。これで、%yearはキーとして月の名前を、値として月数を持ちます。

$num = $year{$month};

 上で"March"や"December"で初期化されている$monthをキーに、ハッシュ%yearを検索します。これで月数が返るので、$numに代入します。

 PerlはTIMTOWTDI(There is more than one way to do it)が身上で、好きなように書いていいので、他にもいろいろ書き方があると思うのでエンジョイしてください。
 ということで、またちゃっかり本の間違いなんかでブログポストを起こすことになりましたが、本当にスミマセンでした。あとsironekotoroさん本当にありがとうございました。今後もよろしくお願いします。