2001/12/10

前回の解答


while ( $days % 7 > 0 ) {
    print "<TD VALIGN=\"top\" WIDTH=\"100\" >  <BR></TD>\n";
    ++$days;
    print "</TR>\n" if ( $days % 7 == 0 );
}
を適当な場所に挿入すればOKです。


ファイルの読み込みと行の処理

一つの事項が1行に記述されているものを考えます。 1行1行読んでいって、それをどこかに記憶しておくのもひとつの方法ですが、 日々のデータを出力するとき、 いちいちデータを最初からよんで該当する日の行を読み込んで処理するほうが 簡単かもしれません。 その月に31日あれば、ひと月分のカレンダーを作るのに、データファイルを 31回読むわけです。

まず簡単のためデータファイル名を data.txt に固定します。 次のような形式のものを扱うことにします:

## data.txt for calendar
REM 20 April MSG 創立記念日Hol!

REM 20 Feb MSG 太郎誕生日
REM  2 Sep MSG 次郎誕生日

REM  5 Mar 2000 MSG 7:00渋谷ハチ公前で待ち合わせ
REM 13 Apr 2000 MSG 5:00池袋いけふくろう前で待ち合わせ
REM 23 May 2000 MSG 買物

REM 13 Fri MSG 13日の金曜日

REM というキーワードで始まる行がデータです。 その他の行は無視することにします。 REM の後に

のうち必要なものだけをスペースで区切って並べます(順不同)。 その後ろの MSG というキーワード以降がその日に関わる情報になります。 すべてのキーワード・データは1個以上のスペースで区切ります。

このファイルを開き、1行1行読んでいく操作は次のようになります:

open (DATA, "data.txt") || die "cannot open data.txt .. $!\n"; 
while ($line = <DATA>) {
    .......... (作業をする)
}
close DATA;

最初の行で、めざすファイルを DATA というスクリプト中で用いる名前(ファイル・ハンドルという)で開きます。 そのファイルが存在しないなどの理由で開けない場合はエラーメッセージを出して終了します。

次に while のループがあります。( ) の中は DATA から1行読み込みその内容を末尾の改行文字も含んだまま $line という変数に代入します。 その値は、読み込めない(ファイルが終わった)場合を除き「真」ですので、 このループは、ファイルの中身がある限り続きます。 読み込んだデータ($line)を用いてブロックの中で作業が行なわれます。

while ループが終了したら(ファイルの終わりにきたら)、close コマンドで閉じます。

実際には、指定した年・月・日に対応する情報を値として返す関数 getmsg を作ることにします:

@monthnames =
("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");

@daynames = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat")
;

sub getmsg {
    local($year, $mon, $day) = @_;
    local($dayname);
    local($monthname);
    local($msg);
    local($line);
    local($msgstr);
    if ($day eq "  ") { return $msg; }
    $dayname = $daynames[&getweek($year, $mon, $day)];
    $monthname = $monthnames[$mon - 1];
    open (DATA, "data.txt") || return $msg;
    while ($line = <DATA>) {
        if ($line =~ /^REM\s+(.*)MSG\s+(.*)/) {
            $msgstr = $2;
            foreach $x (split(/\s+/, $1)){
                $x =~ s/\s//g;
                if ($x =~ /^\d{1,2}$/ && $x != $day) { $msgstr = ""; last; }
                if ($x =~ /^\d{4}$/ && $x != $year) { $msgstr = ""; last; }
                if ($x =~ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat)/i && 
                    $x !~ /$dayname/i ){ $msgstr = ""; last; }
                if ($x =~ /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i &&
                    $x !~ /$monthname/i ) { $msgstr = ""; last;}
            }
            if ( $msgstr ne "" ) { $msg = $msg . $msgstr . "<BR>"; }
        }
    }
    close DATA;
    return $msg;
}

後の便利のために月や曜日の名前(英語)の省略形を配列として導入しておきます。 これはスクリプトの最初のほうでまとめてやっておけばいいでしょう。

サブルーチンでは、まず、3つの引数を $year, $mon, $day の3つの局所変数に格納します。 また、読み込む行を格納する $line、MSG 以降の文字列を格納する $msgstr、 該当するデータのメッセージを順に追加して格納する $msg、 指定した日の曜日や月の英語名を格納する $dayname や $monthname などの局所変数を用意します。

その月の正規な日でない $day に対しては 空文字列を値として返して、 サブルーチンを終了します。 通常の日に対しては上で用意した配列を使って $dayame や $monthname を定めます。 ただし、もともとの getweek 関数は日がついたちに固定されていますので、 適当に書き換えて3つの引数をとるように、あらかじめ準備しておいて下さい(11/26のページの「別解」参照)。

つぎにデータファイルを DATA というファイル・ハンドルで開き、 もしファイルが開けない場合はデータがないということで、空の文字列を値として返してサブルーチンを終了します。

もしファイルが開けた場合は、順に1行ずつ $line に読み込みます。 まず、その行が REM という文字列ではじまり、 しかも途中に MSG という文字列があるかどうかをチェックし、 適合する行の場合のみ作業を行ないます。

       $line =~ /^REM\s+(.*)MSG\s+(.*)/
の =~ がマッチング演算子であることはすでに学びました。 左辺が右辺の条件に適合する場合「真」の値をとります。 右辺の / と / で挟まれた部分にはマッチングのパターンが正規表現で表示されます。 正規表現に関しては次の表を御覧下さい。特別扱いされる
       + ? . * ( ) [ ] { } | \ ^ $
以外の文字はその文字自身にマッチします。 また以下で「英数字」というときは
       A B ... Y Z a b ... y z _ 0 1 2 ... 8 9
を指し、「空白文字」は(半角の)空白・タブ・改行を指します。

  先頭の^ 文字列の先頭 -- 例 ^apple
  末尾の$ 文字列の末尾 -- 例 apple$
  . 改行を除く任意の1文字にマッチする
  (...)括弧内のパターン要素をひとつの要素(グループ)にまとめる
n番目のグループにマッチした部分文字列は$nで表現
  + 直前のパターン要素に1回以上マッチする
  ? 直前のパターン要素に0回または1回マッチする
  * 直前のパターン要素に0回以上マッチする
  {N,M} 直前のパターン要素にN回以上M回以下マッチする
  {N} 直前のパターン要素にちょうどN回マッチする
  {N,} 直前のパターン要素にN回以上マッチする
  [...] 文字クラスのどれかひとつにマッチする --例 [A-Z]
  [^...] クラスに属さない文字ひとつにマッチする --例 [^A-Z]
  (...|...|...) 選択肢のどれかひとつにマッチする
  \英数字以外の文字特別な意味を失い普通の文字になる -- 例 \\, \(, \*, ...
  \w 英数字にマッチする
  \W 非英数字にマッチする
  \b 単語の境界にマッチする
  \B 単語の境界以外にマッチする
  \s 空白文字にマッチする
  \S 空白文字以外にマッチする
  \d 数字にマッチする
  \D 数字以外にマッチする
  \n 改行(new line)
  \r 復帰(return)
  \f 改頁(form feed)
  \t タブ
  \1 \2 ... ( )でまとめたグループにマッチしたサブパターン

もし複数の部分文字列にマッチする場合は最も左で、しかも最も長い文字列がマッチします。

上の getmsg 中の正規表現 ^REM\s+(.*)MSG\s+(.*) の場合、 REM(後に空白文字が続く)ではじまり途中に MSG (これにも空白文字が続く)を含む文字列がマッチします。 REM と MSG の間のデータは $1 という変数に、MSG 以降の文字列は $2 という変数に格納されます。 ここで $2$msgstr という変数に代入しておきます。

$1, $2, ... 以外にも正規表現自体がマッチした部分文字列は $&、それより前の部分は $`、それより後の部分は $' という変数に格納されます。これらも便利です。

foreach のところで split(/\s+/, $1) という配列がありますが、 split は指定したパターン(ここでは \s+)を区切りとして、 文字列(ここでは $1)を分解して配列をつくる関数です。 data.txt の第2行目なら "20" と "April" を要素とする配列ができます。 それらを順次 $x に代入して、$x が指定した日に該当しない場合は $msgstr を空文字列にして、foreach ループから抜け出ます。最終的に 指定する日がその行に該当した場合($msgstrが空でない場合)は、 $msg$msgstr . "<BR>" を追加します。

        foreach $x (split(/\s+/, $1)){
            $x =~ s/\s//g;
            if ($x =~ /^\d{1,2}$/ && $x != $day) { $msgstr = ""; last; }
            if ($x =~ /^\d{4}$/ && $x != $year) { $msgstr = ""; last; }
            if ($x =~ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat)/i && 
                $x !~ /$dayname/i ){ $msgstr = ""; last; }
            if ($x =~ /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i &&
                $x !~ /$monthname/i ) { $msgstr = ""; last;}
        }
        if ( $msgstr ne "" ) { $msg = $msg . $msgstr . "<BR>"; }

あとは表の出力の途中で上の関数を呼び出してやれば、 データを取り込んだ表ができるでしょう。

もし難しければ、データの構造をもっと限定すると、スクリプトの作成が楽になります。 各自工夫して下さい。



今回の課題

上の関数を組み込んで hcal3.pl を作り、サンプルのデータまたは 適当に作成したデータを用いて色々な月のカレンダーを作って下さい。