Pythonでなんか作ってみる -6ページ目

辞書を片手に~人工無能のための正規表現(4)

前回はむりやり正規表現を登場させて、無理やり終わらせたような気もするので、一晩立つと不安がぶり返してきた。RubyとJavaの正規表現記法に違いがないとも限らないし。

最後に『恋するプログラム―Rubyでつくる人工無脳 』に掲載されている正規表現の例を使って不安に止めを刺す。


まずは、こんなの。


 public void testEtc1() {
  assertTrue(match("やあ、ノビィ","^やあ"));
  assertFalse(match("こんやあの娘をモノにするぜ","^やあ"));
 }

テスト実行。Green。もうひとつ追加。 


 public void testEtc2() {
  assertTrue(match("そっちのハナシかよ","かよ$"));
  assertFalse(match("僕はかよわい男です","かよ$"));
 }

なんか同じパターンが見えたのでリファクタリングする。matchCase()を追加する。


 private void matchCase(String regex, String matched, String unmatched) {
  assertTrue(match(matched,regex));
  assertFalse(match(unmatched,regex));
 } 


テストメソッドの中身を関数で置き換える。


 public void testEtc1() {
  matchCase("^やあ","やあ、ノビィ","こんやあの娘をモノにするぜ");
 }
 public void testEtc2() {
  matchCase("かよ$","そっちのハナシかよ","僕はかよわい男です");
 } 


テスト実行。OK。続けよう。


どれか1文字を表す正規表現。


 public void testEtc3() {
  matchCase("こんにち[はわ]","こんにちは","こんにちにゃ");
  matchCase("こんにち[はわ]","こんにちわ","こんにちぱ");
 } 

 

むぅ、また繰り返しがある。matchCaseで文字列の配列を取れるようにしようかと思ったが面倒なので止める。そろそろ、不安が退屈に変わりつつある。


なんでも1文字を表す正規表現。


 public void testEtc4() {
  matchCase("うーッ、...!","うーッ、マンボ!","うーッ、マンボウ!");
  matchCase("うーッ、...!","うーッ、bad!","うーッ、cool!");
 } 


繰り返しを表す正規表現。


 public void testEtc5() {
  matchCase("は+","ははは","ハハハ");
  matchCase("^ええーっ!*","ええーっ!!!","ええーっ!!!");
  matchCase("ふ{3,}","ふふふ","うふふ");
 } 

おや、びっくり。このテストで初のエラーをゲット。

  matchCase("^ええーっ!*","ええーっ!!!","ええーっ!!!");
これのunmatchの方。たしかに”*”は0個以上にマッチするんだから、"ええーっ!!!"にもマッチするよなあ。

誤記か誤植と決め付ける。

このテストは削除する。


あるかないかを表す正規表現。この部分の正規表現は半角と全角が混じってます。


 public void testEtc6() {
  matchCase("いいぞ[!!]?$","よし、いいぞ!","いいぞうさん・・・");
  matchCase("いいぞ[!!]?$","これ使っていいぞ!","その調子だ、いいぞ。");
  matchCase("いいぞ[!!]?$","よし、調子いいぞ","いいぞ!全然漏れてない");
 } 


最後だ。パターンをまとめる正規表現。括弧を使う。


 public void testEtc7() {
  matchCase("(ゴロン)+","ゴロンと横たわる死体","ゴロゴロしたーい");
  matchCase("(ゴロン)+","樽がゴロンゴロンと転がるよ","ゴロゴロしたーい");
  matchCase("(まじ|マジ)で","ま、まじで?","まーじーでー?");
  matchCase("(まじ|マジ)で","マジでそう思います","まーじーでー?");
 } 


これで、どうやら、『恋するプログラム―Rubyでつくる人工無脳 』に使用する範囲の正規表現ではRubyとJavaで同じように動作すると思われる。

しかし、この章の正規表現使用例をテストに起こしてみて気づいたが、マッチする/マッチしない文字列の例は中途半端である。

特に最後の「樽がゴロンゴロンと転がるよ」なんかは、正規表現が「ゴロン」であってもマッチするわけだから、パターンをまとめた意味がない。

せめて、「(ゴロン)+と」まで伸ばしておかなくちゃ。試しにつけてみる。


  matchCase("(ゴロン)+と","ゴロンと横たわる死体","ゴロゴロとしたーい");
  matchCase("(ゴロン)+と","樽がゴロンゴロンと転がるよ","ゴロゴロとしたーい"); 


テスト実行。大丈夫。さて、これだけのパターンを試せば、「文字列がある特徴(パターン)を有するのかどうか判定する」のには十分だろう。


ToDoリスト

・文字列がある特徴(パターン)を有するのかどうか判定する

・”こんにちは、ノビィ”という文字列が”ノビィ”を含むか判定する

・private boolean match(String targetString, String pattern)のリファクタリング

・”ノビィ”もしくは”のびぃ”が含まれるか判定する

・正規表現を使ってパターンに反応するResponder



辞書を片手に~人工無能のための正規表現(3)

さて、いい加減、このやり方にも飽きてきたので一気に片付けることにする。


ToDoリスト

・文字列がある特徴(パターン)を有するのかどうか判定する

・”こんにちは、ノビィ”という文字列が”ノビィ”を含むか判定する

・private boolean match(String targetString, String pattern)のリファクタリング

・”ノビィ”もしくは”のびぃ”が含まれるか判定する


TDDで難しいのは、いつ仮実装を止めて本実装に移るのかというところである。

三角推量という方法もあるが、私が好きなのは次の言葉である。

不安が退屈に変わるまで

上の言葉は、どれだけユニットテストを作成すればいいのかという問題の指標であるが、仮実装から本実装に移る指標にも使えると思う。


ようするに、ちまちまテストに駆動させるのに飽きたら、明白(だと思える実装)に駆動させようということである。ただし、テストは先に必要であるし、5分で動かなければ、変化を破棄してまたちまちま進むことにする。


 public void testNobyOrNoby() {
  assertTrue(match("こんにちは、ノビィ","ノビィ|のびぃ"));
  assertTrue(match("ばいばい、のびぃ","ノビィ|のびぃ"));
 
}

なんの伏線もなく正規表現の記号”|”を使用している。テストはもちろん通らない(と考えているだけでなく、実際に実行して通らないことを確認した)が、明白な実装ってやつを行う。


ちょっと調べてJavaの正規表現が、java.util.regex.Patternjava.util.regex.Matcherであることが分かる。サンプルをちょっと変えて、貼り付けてみる。


 private boolean match(String targetString, String pattern) {
  return Pattern.matches(pattern, targetString);
 }

テスト実行。通らない。

どうやらPattern.matches()は文字列全体と一致しないとtrueを返さないようだ。

べつのサンプルを試してみる。


 private boolean match(String targetString, String pattern) {
  Pattern p = Pattern.compile(pattern);
  Matcher m = p.matcher(targetString);

  return m.matches();
 }

先ほどのとは、意味的に同じなので、当然テストは通らないが、Matcherの別のメソッドを使える。


 private boolean match(String targetString, String pattern) {
  Pattern p = Pattern.compile(pattern);
  Matcher m = p.matcher(targetString);

  return m.find();
 }

テスト実行。Green!!出来てしまった。

後の正規表現(”.”とか”*”とか”[]”とか)の確認はとりあえずパス。

ToDoを更新して次回に備える。


ToDoリスト

・文字列がある特徴(パターン)を有するのかどうか判定する

・”こんにちは、ノビィ”という文字列が”ノビィ”を含むか判定する

・private boolean match(String targetString, String pattern)のリファクタリング

・”ノビィ”もしくは”のびぃ”が含まれるか判定する

・正規表現を使ってパターンに反応するResponder

辞書を片手に~人工無能のための正規表現(2)

さて、続きである。正規表現のはずなのに中々正規表現が出てこない。

だらだらやっても仕方ないので、リファクタリングに入る。


ToDoリスト

・文字列がある特徴(パターン)を有するのかどうか判定する

・”こんにちは、ノビィ”という文字列が”ノビィ”を含むか判定する

・private boolean match(String targetString, String pattern)のリファクタリング


match()の処理は結局”ノビィ”が文字列に含まれているかどうかを判定しているだけなので、indexOfを使えば簡単になる。


 private boolean match(String targetString, String pattern) {
  if (targetString.indexOf(pattern)>0) return true;
  return false;
 }
 

テスト実行。失敗!!おっと間違えた、indexOfは文字列の最初に存在する場合は0を返すんだな。

こういうのはExcelのマクロをいじっていると1文字目が0なのか1なのか混乱する。修正。


 private boolean match(String targetString, String pattern) {
  if (targetString.indexOf(pattern)>=0) return true;
  return false;
 }
 

今度はOK。

しかし、やっぱり正規表現は出てこない・・・。


辞書を片手に~人工無能のための正規表現(1)

さてさて、お久しぶりです。

アメブロはメンテしているかと思ったら延期になっていたんですね。

そうと知っていたら、バリバリ記事の続きを書いたのに(嘘)


では早速、前回予告したとおり、正規表現である。

そういえば、私はJavaであまり正規表現を使わない。grepやsedやperlでは当然使うが、Javaで文字列検索をするようなプログラムを書かないからだろう。


恋するプログラム―Rubyでつくる人工無脳 』では「パターンマッチ」のために、この正規表現を使うと書いてある。

パターンとはなにか?

厳密には異なる二つ以上の物に、共通して存在する特徴のことである。

正規表現とは、ある文字列パターンの特徴を表す式のことである。その式を記述することで、任意の文字列がその特徴を持っているかどうかを判定することができる。


この章では、Rubyで正規表現を扱う方法について説明している。単純に考えれば、RubyをJavaに置き換えてやってみればいいだけなのだが、それではこの記事のテーマが「Javaを使った人工無能製作」になってしまう。

あくまでもテーマは「TDD」で無ければならない。


『私は何をしたいのだろうか?』


正規表現というのは、あくまでも目的を達成するための手段に過ぎない。いうなれば実装に相当する。

正規表現が目的では無い以上、本当の目的はなんなのか?

こう自問するのがTDDである。

答えは既に書いている。

 

・文字列がある特徴(パターン)を有するのかどうか判定する


具体的で無いせいかピンとこない。具体的な例でリストを補強する。


 ToDoリスト

・文字列がある特徴(パターン)を有するのかどうか判定する

・”こんにちは、ノビィ”という文字列が”ノビィ”を含むか判定する


うん、具体的になった。これでようやくテストを書ける。


public class PatternMatchTest extends TestCase {
 public void testNoby() {
  assertTrue(match("こんにちは、ノビィ","ノビィ"));
 }
} 


matchが無いのでコンパイルエラー。即時修正する。 


 private boolean match(String targetString, String pattern) {
  return true;
 }
 


テスト実行。Greenである。偶然ではあるが、テストに通ってしまった。TDDでは失敗するテストが無ければ先に進めない。テストを追加する。


 public void testNoby() {
  assertTrue(match("こんにちは、ノビィ","ノビィ"));
  assertFalse(match("ばいばい、のびぃ","ノビィ"));
 }
 


テスト実行。今度こそRed。テスト失敗である。これでようやく先へ進める。

仮実装を行う。


 private boolean match(String targetString, String pattern) {
  if (targetString.equals("こんにちは、ノビィ")) return true;
  return false;
 } 


これで、バーがGreenに戻る。さて、リファクタリングに取り掛かる。

仮実装の方のequalsの引数となっている”ノビィ”は、実はpatternのことである。置き換える。


 private boolean match(String targetString, String pattern) {
  if (targetString.equals("こんにちは、"+pattern)) return true;
  return false;
 }
 


テスト実行、Greenのまま。しかし、これ以上テストが導いてくれる感じがしない。そこで、テストを追加することにする。


 public void testNoby() {
  assertTrue(match("こんにちは、ノビィ","ノビィ"));
  assertTrue(match("ノビィ~るジーンズ","ノビィ"));
  assertFalse(match("ばいばい、のびぃ","ノビィ"));
 }
 


Redに戻ってしまうが先に進める。仮実装を追加する。


 private boolean match(String targetString, String pattern) {
  if (targetString.equals("こんにちは、"+pattern)) return true;
  if (targetString.equals(pattern+"~るジーンズ")) return true;
  return false;
 }
 


Greenにはなるが、これ以上どうやればパターンマッチが出来るのか?

よく見ると、match()の1行目と2行目はほとんど同じ内容である。ここをリファクタリングするべきだろう。


続く




NULLチェック

取り出してから呼ぶか、Iterator で呼び出すか

本当は@niftyへ直接コメントを書けばいいのでしょうが、休眠(利用停止)させたIDの復活方法が分からなかったのと、新IDを@niftyの接続サービスを利用しないで取得する方法が分からなかったのでここに書きます。

こういうのもトラックバックになるのでしょうか?


> これなら、

>     overwriteBbsInfo(getIncludeList().iterator());
>
> で呼べるから便利なのだが、最初からそう書けよという話。ただ、話はここで終わらない。List が null のときの処理が必要になったわけだが、すると、

>     private void overwriteBbsInfo(Collection collection) {
>         if (collection == null)
>             return;
>         Iterator it = collection.iterator();
>         while (it.hasNext()) {
>            BbsInfo info = (BbsInfo) it.next();
>             // bbsList に info と一致する要素があれば、
>             // info を使って上書きする。
>         }
>     }

いつのまにかoverwriteBbsInfo()がIteratorではなくCollectionを受け取るようになっていますね。


まあ、自分ならListがnullのときはgetIncludeList()では空のColletionを返すようにします。

これもヌルオブジェクトの最初の一歩とも言えますよね?



 

訪問者とランキング

昨夜、TDDの方でひと段落つけると若干燃え尽きっぽくなってしまいました。

仕事のときでも同様の症状に見舞われるのですが、そんなときは、次のアイテムに手を出すのを躊躇して、別の無駄な作業(まあ2chとか)にばかり時間を使ってしまう。

まあ、このTDDの記事はやる気がないときはやる必要もないわけだけど、毎日書かないとランキングに響くかなとか考えてしまいます。


別にランキングにこだわりがある訳ではないのだけど、開設してから順調に昇っていっているようだと、やっぱり意識してしまいますよね。いや、やっぱり十分こだわっているのかも。


で、管理画面から「訪問者とランキング」の画面に移るとランキングの推移がグラフ表示されるのだけど、このグラフなんかおかしく無いですか?特にジャンルランキングのグラフが顕著です。

縦軸が極端な対数となっているのかどうかは知りませんが、週前半の300位ぐらいの差と、昨日と本日の差の30位が同じぐらいの差で表示されています。


結果的に、突然ランキングがあがったような印象を与えるグラフとなっています。他の人もそうなのでしょうか?ジャンルランキングのグラフは始めてみたので常にそうなっているのかは分かりませんが。


ブロガーのやる気を増すためにそういう仕様で表示しているのか、システムの欠陥なのか判断が付け辛いですが、どうなんでしょう?

辞書を片手に~辞書はファイルに(5)

さて、『恋するプログラム―Rubyでつくる人工無脳 』の方のこの章では、あとは配列からランダムに応答を選択する部分をメソッドに切り出したりしているのだが、実装が難しくない且つテストが難しい(ランダムを扱うのは難しいのだ)ため省略する。(この部分をTDDで実装するよりも先へ進みたいので。)


最後に、この章のまとめを記録しておく。


・最初の実装時に一般的過ぎる設計を思い浮かべてしまうとテストが駆動しない

・当初loadDictionary()メソッドはファイル名をInputにしていたが、テストに駆動させた結果、FileReaderがInputになった。この方が良い設計に思える

・やっていることを見失わないようにToDoリストを使用する

・テストは分離して記述した方が良いが、ついつい面倒で既存のメソッドに書いてしまう

・RandomResponderのloadDictionaryはstatic public のままであり、つまりメソッドが存在すべき適切な位置では無い気がする


次回は、正規表現を使ったパターンに反応するResponderの作成である。

辞書を片手に~辞書はファイルに(4)

ToDoリスト

fisrt.datファイルから3つの応答メッセージを読み込む

add.datファイルから4つの応答メッセージを読み込む

・辞書ファイルの名前はrandom.txt

・ファイルの最後の応答メッセージが改行で終了している場合 

 

さて、残るは辞書ファイルの名前であるが、loadDictionary()はファイル名に依存していない。

では、誰が辞書ファイル名の責任を持っているのかと言えば、ファイル名がrandomであることからも、RandomResponderクラスであることは明白であろう。

そういえば、今まではloadDictionary()はテストクラスに書いていたが、このままでは使用できない。このメソッドはRandomResponderのメソッドであるべきなのだ。


ToDoリスト

・辞書ファイルの名前はrandom.txt

・loadDictionary()をRandomResponderへ移動する


まずは、今追加した方から。

とりあえず、static publicで良いだろう。テストを修正する。


 public void testLoadDictionary() throws FileNotFoundException {
  assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"}, RandomResponder.loadDictionary(new FileReader("dics/first.dat")));
  assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
    RandomResponder.loadDictionary(new FileReader("dics/add.dat")));
 }

当然コンパイルが通らなくなるので、空実装を追加する。EclipseではCtrl+1のQuickFixで即効である。


 public static String[] loadDictionary(FileReader reader) {
  return null;
 }

テスト実行。当然Red。ぬるぽである。loadDictionaryの実装を移す。


 public static String[] loadDictionary(FileReader reader) {
  BufferedReader r = new BufferedReader(reader);
  try {
   ArrayList response = new ArrayList();
   while (true) {
    String line = r.readLine();
    if (line == null) break;
    response.add(line);
   }
   return (String[])response.toArray(new String[]{});
  } catch (IOException e) {
   return new String[] {};
  }
 }

ちょっと引数名が変わっているが、愛嬌ということで。テスト実行。Green。

さて、これで残るは一つである。まずはrandom.txtを作成する。これはfirst.datをコピーして作った。

次にRandomResponderのテストを作成。


 public void testRandomResponderResps() throws FileNotFoundException {
  RandomResponder rr = new RandomResponder("random");
  assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった"}, rr.resps);
 }


loadDictionaryのテストをコピペして修正した。

テスト実行してみるとGreenである。

テストが失敗しなければ、機能追加は出来ないのでは?そう、そのとおりである。

では、失敗するテストを追加するのか、というと、そういう方法もあるが、もう一つ方法がある。リファクタリングである。

RandomResponderを良く見ると、重複コードがあるのだ。どこか?


public class RandomResponder extends Responder {

 String [] resps = {"今日はさむいね","チョコたべたい","きのう10円ひろった"};
 Random rnd = new Random();
 public RandomResponder(String name) {
  super(name);
 }
 /* (non-Javadoc)
  * @see proto.Responder#response(java.lang.String)
  */
 public String response(String msg) {
  return resps[rnd.nextInt(resps.length)];
 }
 /**
  * @param reader
  * @return
  */
 public static String[] loadDictionary(FileReader reader) {
  BufferedReader r = new BufferedReader(reader);
  try {
   ArrayList response = new ArrayList();
   while (true) {
    String line = r.readLine();
    if (line == null) break;
    response.add(line);
   }
   return (String[])response.toArray(new String[]{});
  } catch (IOException e) {
   return new String[] {};
  }
 }
}

それは、ここである。


 String [] resps = {"今日はさむいね","チョコたべたい","きのう10円ひろった"};


ファイルから読み込む機能は、先ほどのメソッドの移動によって実装したと考えることができ、そう考えるとこのデータ定義部分はrandom.txtの内容と重複していることになるのだ。

つまり、この部分の重複を無くすという目的でリファクタリングを行う。(詭弁っぽい?確かにそのとおりである。)

では、resps設定処理をリファクタリングする。


 public RandomResponder(String name) {
  super(name);
  try {
   resps = loadDictionary(new FileReader("dics/random.txt"));
  } catch (FileNotFoundException e) {
   resps = new String[]{""};
  }
 }


また、例外を捕捉するかthrowsするかの選択を迫られたが、今回はコンストラクタ内で捕捉して、空応答一つのみを返す、実装にする。この実装はちゃんと動くかちょっと不安なのでToDoに追加しておく。


ToDoリスト

・辞書ファイルの名前はrandom.txt

loadDictionary()をRandomResponderへ移動する

・辞書ファイルがない場合


では、テストを実行。Greenのまま。respの初期化しょりを削除する。テスト実行。Greenのまま。


続いて辞書ファイルがない場合のテストを追加するのだが、テストのためにrandom.txtを作成したり削除したりするのは大変面倒である。それよりはRandomResponderを作成時に辞書ファイルを指定できるようにしておく方がいいのではないかと考える。


ToDoリスト

・辞書ファイルの名前はrandom.txt

loadDictionary()をRandomResponderへ移動する

・辞書ファイルがない場合

 ・RandomResponder生成時に辞書ファイル名を指定できるようにする


まずは、辞書ファイル指定型コンストラクタを追加する。おっと間違えた。えーと、辞書ファイルを指定した場合のテストを書く。


  RandomResponder rr2 = new RandomResponder("random","add.dat");
  assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
    rr.resps);


せっかくだから、add.datを使ってみた。

コンストラクタが無いと怒られるので、実装。今度は明白な実装に思えるので仮実装を飛ばす。


 public RandomResponder(String name,String fileName) {
  super(name);
  try {
   resps = loadDictionary(new FileReader("dics/"+fileName));
  } catch (FileNotFoundException e) {
   resps = new String[]{""};
  }
 }


テスト実行。Red!!

おかしいな、どこでしくじった?


junit.framework.AssertionFailedError: expected:<4> but was:<3>
.....

 at proto.FileReaderTest.assertEquals(FileReaderTest.java:40)
 at proto.FileReaderTest.testRandomResponderResps(FileReaderTest.java:32) 


テスト結果によれば、3つしか読み込めてない模様。おっとテストを見直すとrrでテストしている。ここをrr2に変更する。 


  RandomResponder rr2 = new RandomResponder("random","add.dat");
  assertEquals(new String[] {"今日はさむいね","チョコたべたい","きのう10円ひろった","それからそれから?"},
    rr2.resps);
 


こういうテストミスが実は結構多い。ちゃんとテストごとにテストメソッドを分ければ起きないミスだが、面倒がって既存のテストメソッドに含めると良くやる。

でもまあ気を取り直して、RandomResponderを見ると、コンストラクタの処理に重複が見える。

リファクタリングだ!!

ここはJavaのイディオムだと思うのだが、引数の少ない方のコンストラクタが引数の多い方のコンストラクタを呼び出すように変えればよい。 


 public RandomResponder(String name) {
  this(name,"random.txt");
 }
 


テスト実行。Greenのまま。リファクタリング成功である。

あとは残ったケース、辞書ファイルがない場合のテストを追加する。 


  RandomResponder rr3 = new RandomResponder("random","none.dat");
  assertEquals(new String[] {""}, rr3.resps); 


テスト実行。Greenである。これで全てのToDoを消化した。

デフォルトの文字色

今、気づいたが、メッセージボードのデフォルトの文字色が灰色っぽいようである。

まあ、それはいいのだが、その色が上の文字パレットに無い(正確には全色表示でやればあるのだろうが)のである。

で、ここでTDDの記事を書くときは、赤(エラーメッセージ用)とか青(コード用)とか多用するのであるが、そこから本文用に戻すときに黒を押していたのである。


後から見直すと、黒と灰色が入り乱れていて格好悪い。

設定画面を見ても標準の文字色の設定パラメータはないし、CSSを変更しなければならないの?


標準色のボタンが欲しいのだが。


仕方ないので、CSSを変更して、bodyの文字色を黒(#000000)にした。

ということは、後からこの記事を読んでも、何のことだか分からない可能性が高いわけだ。

困ったもんだ。

辞書を片手に~辞書はファイルに(3)

ToDoリスト

・fisrt.datファイルから3つの応答メッセージを読み込む

・add.datファイルから4つの応答メッセージを読み込む

・辞書ファイルの名前はrandom.txt


早速前回の教訓を生かして、ToDoリストを作った。

では、first.datから3つの応答メッセージを読み込む本実装といこう。

first.datを作る。1行1メッセージの簡単なフォーマットである。


今日はさむいね
チョコたべたい
きのう10円ひろった


実装だが、1行ずつ読み込むというのは、U/Iの部分に書いたところと似ている感じがする。

とりあえず、コピぺしてみる。


 private String[] loadDictionary(FileReader file) {
  BufferedReader r = new BufferedReader(file);
  try {
   String response = r.readLine();
   
   return new String[] {response};
  } catch (IOException e) {
   return new String[] {};
  }
 }

仮実装を丸ごと削除して、入れ替えてみた。どう考えても1行しか読み込まないが、一応テストを実行してみる。


junit.framework.AssertionFailedError: expected:<3> but was:<1>

確かに1個しか読み込んでいないみたいだが、その1個目が成功しているかどうかをしりたい。

assertEquals(String[] ,String [])を書き換えてみる。

 public void assertEquals(String[] expected, String[] actual) {
  for (int i=0;i<Math.min(actual.length,expected.length);i++) {
   assertEquals(expected[i],actual[i]);
  }
  assertEquals(expected.length,actual.length);
 }

最初に存在する分を比較して、その後、配列数を比較するように変更した。テスト実行。出力結果は変わらないが、つまり1個目は無事読み込めたということだ。

すべての行を読み込むように修正してみる。

ループさせて各行を読み込むようにすれば良いのだが、Javaでは配列に後から要素を追加出来ただろうか?Arrayクラスを導入するか?

いやいや、それはまだ飛躍しすぎな気がする。今は3つの要素を読み込めればよいのだ。


 private String[] loadDictionary(FileReader file) {
  BufferedReader r = new BufferedReader(file);
  try {
   String[] response = new String[3];
   for (int i=0;i<response.length;i++) {
    response[i] = r.readLine();
   }
   return response;
  } catch (IOException e) {
   return new String[] {};
  }
 }

テストを実行すると失敗位置が変わる。2つ目のテストで失敗である。

junit.framework.ComparisonFailure: expected:<今日はさむいね> but was:<null>
 

add.datの方にデータを追加する。今度は、4つの応答メッセージである。


今日はさむいね
チョコたべたい
きのう10円ひろった
それからそれから?


テストを実行。Redのまま。今度の失敗メッセージは3つしか読めていないという意味。


junit.framework.AssertionFailedError: expected:<4> but was:<3>

試しに、loadDictionary()の読み込み数を4にしてみる。

junit.framework.AssertionFailedError: expected:<3> but was:<4>

うん。今度は1つ目のテストが応答数が異なると出る。

いよいよ可変長の配列を使用する必要があるということか?

とりあえずArrayListクラスを使ってみる。

  private String[] loadDictionary(FileReader file) {
  BufferedReader r = new BufferedReader(file);
  try {
   ArrayList response = new ArrayList();
   for (int i=0;i<4;i++) {
    response.add(r.readLine());
   }
   return (String[])response.toArray(new String[]{});
  } catch (IOException e) {
   return new String[] {};
  }
 }

テスト実行。やっぱりRed。メッセージ内容は同じ。そりゃそうである。4回まわしているんだから。

ループを固定数でなくファイル内の行数でまわす必要がある。

ドキュメントによるとファイルの終わりに達している場合はreadLineはnullを返すらしい。そうなのか?気づかなかった。


 private String[] loadDictionary(FileReader file) {
  BufferedReader r = new BufferedReader(file);
  try {
   ArrayList response = new ArrayList();
   while (true) {
    String line = r.readLine();
    if (line == null) break;
    response.add(line);
   }
   return (String[])response.toArray(new String[]{});
  } catch (IOException e) {
   return new String[] {};
  }
 }

こんなんで良いのか?と半信半疑でテスト実行。Green。おや、出来てしまった。

さて、気になるのは、ファイルの終わりの判定がこれで良いのかどうかである。first.datを見直してみると、最後の応答メッセージ「」の終わりに改行が付いていない。ここに改行をいれるとどうなるのか?

おっと、その前にToDoリストを見直してみて、終わった項目を消す。


ToDoリスト

fisrt.datファイルから3つの応答メッセージを読み込む

add.datファイルから4つの応答メッセージを読み込む

・辞書ファイルの名前はrandom.txt

・ファイルの最後の応答メッセージが改行で終了している場合 


では今追加した条件を早速試す。テスト実行。Green。

要らぬ心配だったようだ。