フィンローダのあっぱれご意見番 第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「本文とか注釈に該当するパラグラフがない場合です。 パラグラフがなくても省略はできないので、 空の本文とか注釈を想定したのですが、 実際はどうすればいいでしょうか?」 ふ「 のような文字列を出力する手がありますね。 とりあえず、これでよいことにしましょう。 では早速、このような出力を生成するプログラムを考えるのですが、 最初から全部考えるのも面倒なので、 “(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.