Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第154回「※注(2)」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

今回は、前回の続きである。 前号を持っていない方は話が見えないだろうから、 あらすじだけ紹介しておくと、 一度公開したページに注釈を付けたページを作りたい、という話である。 横書きで、注釈を対応する本文の横に表示したい。 そのためにHTMLのテーブルを使って表現するのだが、 手書きは無理だから、 プログラムを書いて処理しよう、 ということだった。

  

実は「あっぱれご意見番」は、ネットでも公開している。 多分、プログラマーズフォーラムあたりで見ていただけると思う。 この原稿を書いている時点で、 フォーラムがWebに移転するということで、大パニック状態なのだが、 本誌が書店に並ぶ頃には何とかなっているだろう。

 

※ これを書いている時点では FPL で公開することは決まっていなかった。

§

前回、ややこしい例として、 (1) 注釈、(2) 注釈、(3) 本文、(4) 注釈、(5) 本文、 (6) 本文、 (7) 本文、 (8) 注釈、 (9) 本文、 の順にパラグラフが出現した場合を紹介した。 fig.1 のように段組できたら成功である。 なお、fig.1 の横線は見やすくするために描いてあるだけで、 実際のページの表には罫線は表示しない。

---- fig.1 ややこしい例 ----

 ――――――――――――――――――
              (1) 注釈
              (2) 注釈
 ――――――――――――――――――
 (3) 本文         (4) 注釈
 ――――――――――――――――――
 (5) 本文
 (6) 本文
 ――――――――――――――――――
 (7) 本文         (8) 注釈
 ――――――――――――――――――
 (9) 本文
 ――――――――――――――――――

注釈を本文の横に表示する理由は、 注釈がどの本文に対応しているかを、 横にあるという情報を使って表現するためである。 だから、(3)と(4)、(7)と(8)のように 本文と注釈が横に並ぶことは重要だ。 実は、 (5)、(6)のように本文が続いたときに1つの行にまとめる処理が難しい。 本文をすべて別の行にしてしまう手もあって、 その方が処理は簡単になる。 しかし、今回は、あえて(5)と(6)をまとめて1行に入れることにする。

§

ふ「念のため、 TABLEとはどのような構造なのかを、先に確認しておきましょう。 fig.2 を見てください。」

---- fig. 2  TABLE の構造 ----

 <table>
  <tr><td>本文1</td><td>注釈1</td></tr>
  <tr><td>本文2</td><td>注釈2</td></tr>
  … (必要なだけ繰り返す) …
 </table>

U「HTML で見かける、一般的な形式の表ですね。」

ふ「<table> と </table> で囲まれた部分が表の内容です。 <tr> と </tr> で囲まれた部分は、表の上での1行に相当します。 この1行というのは、 表の行・列で数えたときの1行という意味です。」

U「画面上の文章の表示は、 折り返して複数行になることがありますね。」

ふ「1行の中はどのような構造になっていますか?」

U「<td>と</td>で囲まれた部分を2つ持っています。 この2つは、順に本文、注釈、に対応しています。」

ふ「必ず本文、注釈の順に現れることに注意してください。 さて、今回作ろうとしているプログラムは、 先ほどの例に対して、どのように出力すればよいでしょうか?」

U「fig. 3 で、どうでしょうか。」

---- fig.3 ややこしい例に対応した出力例 ----

 <table>
  <tr><td>(空)</td><td>(1) 注釈 (2) 注釈</td></tr>
  <tr><td>(3) 本文</td><td>(4) 注釈</td></tr>
  <tr><td>(5) 本文 (6) 本文</td><td>(空)</td></tr>
  <tr><td>(7) 本文</td><td>(8) 注釈</td></tr>
  <tr><td>(9) 本文</td><td>(空)</td></tr>
 </table>

ふ「(空)というのは?」

U「本文とか注釈に該当するパラグラフがない場合です。 パラグラフがなくても省略はできないので、 空の本文とか注釈を想定したのですが、 実際はどうすればいいでしょうか?」

ふ「&nbsp; のような文字列を出力する手がありますね。 とりあえず、これでよいことにしましょう。 では早速、このような出力を生成するプログラムを考えるのですが、 最初から全部考えるのも面倒なので、 “(5)本文”を処理するところから考えてみてください。」

U「まず、“(5)本文”を読み込みます。 直前のパラグラフは“(4)注釈”なので、 現時点で処理中の行を閉じて、次の行を始めることになります。」

ふ「そのような処理を、 今回は「改行(する)」と表現することにしましょう。」

U「はい。 では言い換えると、 “(5)本文”を読み込んだら、 直前が注釈なので、 改行して、“(5)本文”を出力する、 という処理をします。」

ふ「いいですね、続けましょう。」

U「“(6)本文”を読み込みます。 直前が“(5)本文”なので、 本文が続きます。 これらはまとめて1つの行にしますから、 そのまま“(6)本文”を出力します。」

ふ「とりあえず続けてください。」

U「“(7)本文”を読み込みます。 同様に、“(7)本文”を出力します。」

ふ「fig.3 では、“(6)本文”と“(7)本文”は別の行になっていますが?」

U「ありゃ、本当だ。 “(7)本文”は今までの本文とは別にして、 次の“(8)注釈”と同じ行にしなければなりませんね。 では訂正して、 “(7)本文”を読み込んだら、空の注釈を追加して、改行して、 “(7)本文”を出力します。」

ふ「まだ“(8)注釈”は読み込んでいませんが、 なぜ次に注釈が来ると分かりましたか?」

U「そう言われてみると不思議ですね。 当たり前ですが、(8)を読まないと、(8)が注釈かどうかは判断できません。 つまり、(7)をどう処理するかは、(8)を読まないと判定できないのですか。」

ふ「そうですね、 本文が連続して出てくる場合には、最初の本文を除いて、 次に何が来るか分かるまで処理できないことになります。」

U「ということは“(6)本文”の処理も誤りで、 たまたまうまく行っただけでしたか。」

ふ「実はそうなのです。」

§

このように、先読みしておかないと処理できないような問題は、 現実によく発生する。 どうやって解決するかというと、 もちろん先読みするしかないのだが、 具体的な対処法がいくつかある。 とりあえず典型的なものを3つ紹介しよう。

・データを先読みしておく、または処理を次のデータを読むまで保留する。

・処理中に後戻りできるような仕組みを用意する。

・先読みしなくてもいいように全体を仮処理してから、もう一度全体を処理しなおす。

今回は、データを先読みするアプローチを考えてみる。 まず、先読みする処理を考えておこう。

---- List 1 次のパラグラムを読み込む処理 ----

    private Paragraph getNextParagraph() {
        Paragraph p = nextParagraph; // 既に読んであるパラグラフ
        nextParagraph = getParagraph(); // パラグラフを読み込む
        return p;
    }

    private Paragraph nextParagraph = null; // 初期化は省略できる
    private Paragraph lastParagraph = null;

今回は Java っぽく書いている。 Paragraph というクラスがあることを想定している。 getNextParagraph() を呼び出すと、次のパラグラフを返してくれるが、 この時に、実は1つ先のパラグラフを読んでいて、 返すパラグラフは1度前に読んだパラグラフ、という仕組みだ。 ただし、全体の処理を始める前に、1度だけ先に呼び出しておく必要があるので、 その点は注意が必要になる。

この処理がやっていることは単純である。 複雑な処理を書くコツは、 このように、単純な処理に分解して考えることだ。 1つの処理の中で全部やろうとすると、破綻するか、 たまたまうまく行っても、保守できないか、というオチになってしまう。

この処理を使えば、 パラグラフを順に読み込んで処理する部分は、、 List 2 のような感じに書ける。

---- List 2 パラグラフ単位の処理 ----

    Paragraph p = getNextParagraph(); // 先に1度呼んでおく
    while ((p = getNextParagraph()) != null) {
        if (p.isBody()) {
            addBody(p); 
        else {
            addNote(p);
        }

        lastParagraph = p; // 後で使うので保持しておく
    }

細かいことは説明しない。 各関数の振る舞いは、名前から想像してほしい。 問題は、addBody() と addNote() の中身だ。 addBody() の実装例を List 3 に示す。

---- List 3 パラグラフが本文のときの処理 ----

    /**
     * 本文の処理
     */
    private void addBody(Paragraph p) {
        if (isPrevBody()) { // 直前が本文なら、
            if (isNextNote()) { // 次が注釈なら、
                feedCurrentLine(); // 改行する
            }
        } else if (isPrevNote()) { // 直前が注釈なら
            feedCurrentLine(); // 改行する
        }

        addParagraph(p);
    }

    /**
     * 注釈の処理
     */
    private void addNote(Paragraph p) {
        if (isPrevBody()) { // 直前が本文なら、
            addSeparator(); // "</td><td>" を追加する
        } else if (! isPrevNote()) {
            // 直前が注釈でも本文でもなければ、空の本文を追加
            addEmptyBody();
            addSeparator();
        }

        addParagraph(p);
    }

isNextNote() は、続くパラグラフが注釈(note)かどうかを判定する処理で、 List 4 のようになる。

---- List 4 isNextNote の実装 ----

    private boolean isNextNote() {
        if (nextParagraph == null)
            return false;
        return lastParagraph.isNote();
    }

ここで nextParagraph を使っているのだが、 これこそが、 getNextParagraph() でこっそり先読みしておいたパラグラフだ。

ちなみに、この処理には、二つの落とし穴がある。 この処理全体が始まるときの「直前に処理したパラグラフ」、 そして、最後のパラグラフを処理するときの「次のパラグラフ」だ。 いずれも実在しないのである。 List 3 が、この落とし穴をうまく回避していることを確かめて欲しい。 長くなったので、残りの2つの方法については、また次回に。

  

(C MAGAZINE 2005年4月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-01 裏ページに転載。

(C) Phinloda 2005-2006, All rights reserved.