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

フィンローダのあっぱれご意見番 第169回「比べるということ (2)」

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

Jakarta commons。 Java で楽してプログラムを書きたいのなら、 忘れちゃいけない。 とはいっても覚えてもいられなかったりするが、 という訳で、 org.apache.commons.lang.math.Range というクラスがある。 Jakarta には、commons Math というパッケージもあるのだが、 今回紹介するのは commons.lang.math なので、 間違えないように。 commons Math の API ドキュメントとか見ると、 Range というクラスがないので、 おやっ、ということになってしまうので注意。

Range というのは要するに範囲を表現するためのクラスである。

もう少し説明すると、 Range というのは、 最大値と最小値の2つの数値を持つオブジェクトを扱うためのクラスである。 これを基底クラスとして、 IntRange とか、 DoubleRange とか、 いくつか実際的な派生クラスが用意されている。

Range の話に入る前に、 単純な値を比較した結果としてはどんな情報が欲しいのか、 少し考えてみよう。 最も単純な場合は、 一致するか、しないか。 この2通りの結果を boolean で受けたい。 大小をきっちり知りたいなら、 どちらが大きいかという情報も返せばいい。 この場合は3通りの戻り値を用意することになるので boolean では受け切れない。 int で受けて、-1、0、1、というのが定番である。

§

Range の場合、一つのオブジェクト内に2つの値が含まれているから、 その分、多少ややこしい。 単に大小の関係だけではなく、 含まれているとか、重なっている、という状態があるのだ。 例として、 IntRange にある特徴的なメソッドを、 いくつか紹介してみよう。

boolean containsInteger(int value)

与えられた int の値が、 この IntRange オブジェクトの範囲に含まれるかどうかを判定するメソッドだ。 私見ではあるが、このメソッドの名前はイヤだ。 int value を引数にするのなら、 containsInt という名前にして欲しい。 containsInteger という名前から予想するのは、 誰が何といおうと Integer の引数なのである。

ま、その話はおいといて、もう一つメソッドを紹介する。

boolean containsRange(Range range)

指定された Range がこの Range に含まれているかどうかを判定するメソッドである。 例えば、1~100という範囲には、10~20という範囲が含まれているので true だが、 10~110という範囲は少しはみ出てしまうので false、といった結果になる。

containsRange は完全な包含関係を判定するメソッドだが、 範囲を扱うときには、 もう一つ、よく使われるメソッドがある。 重なっているかどうかの判定だ。

例えば、13:00~15:00 の時間帯で会議室を予約するときに、 他のチームが 14:00~18:00 で予約を入れていたら予約できない。 というような判定をするときには、 重なっているかどうか、という判断が必要になる。 このときは、 overlapsRange というメソッドを使う。

public boolean overlapsRange(Range range)

§

さて、Range クラスに用意されているメソッドはこれだけだ。 いや、他にもいろいろメソッドは用意されているのだが、 比較する目的で直接使うようなメソッドはこれ位なのである。

例えば、 この Range がある値より大きいか、というようなメソッドは用意されていないのだ。 もっとも、そのような比較は簡単に実装できる。 ある値、value が、Range range に含まれるどの値よりも小さいことを調べるには、 List 1 のようにすればいい。

---- List 1 ----

    if (value < range.getMinimumInteger) {
        hoge();
    }

---- List 1 end ----

では、value が range のどの値よりも大きいことを調べるには?

---- List 2 ----

    if (value > range.getMaximumInteger) {
        hoge();
    }

---- List 2 end ----

簡単ですね。

§

  

「間違いだらけのC」という本を書こうとしたことがあった。 というか実は今でも書きたいと思っている。 どんな本か。 この種のタイトルの本でよくあるのは、 「C言語によくある間違いを紹介して、正しくはどう書くのか」を書いた本なのだが、 私の意図しているのはそんな本ではない。 単に中に書いてあることが間違っているだけという本を書きたいといっているのだ。 うっかり読んで鵜呑みにしたら、 とてつもなくひどい目にあう、という代物である。

 

※ 「間違いだらけのCプログラミング」という本が既に存在するそうだ。 内容に関しては読んでいないので評価できないが、 常識的に考えたら、間違った例を示しておいて、正しく書くにはどのように修正するか、 というような内容ではないかと想像する。

まあそんな本は企画も何も通らないわけでどうでもいいが、 さて、先に書いた内容だが「簡単ですね」と簡単に締めくくってみたのだが、 どこに罠があったか、皆さんは気がつきましたか?

  

という訳で話はどんどん横道に向かう。 数学の話をしよう。 数学といっても中学生、いや、 少し算数が好きなら小学生でも分かるレベルの話なのでご安心を。 「~以下」と「~未満」の話である。

 

※ あまり難しい話は、私にも分からないので書けないさ。

「20歳未満はお使いになれません」とか、 「あなたは18歳以上ですか?」のような確認の質問が書かれたサイトを見たことがあると思う。 Range の話題としては、片方だけの値しかないとちょっと違和感があるが、 年齢の場合は 0 から始まるという大前提があるから、 0~20、というように考えてもいいはずだ。

ここで本質的な問題はただ一点である。 Range の端の値は含むのか、含まないのか、ということなのだ。

以下というのは、 指定された値も該当することになっている。 未満は、指定された値は含まないことになっている。

実は、Range を扱うときには、 多くの場合、この「両端の値」問題は避けて通れないのである。

先ほど紹介した、会議室の予約を考えてみよう。 14:00~18:00に予約が入っていたので、 13:00~15:00に予約を入れることができなかった。 この例は、 14:00~15:00 という1時間がバッティングするから、 予約できないことは明白だ。 では、13:00~14:00に予約を入れたくなったら?

一般常識で考えてみよう。 14:00~18:00に予約が入っている会議室に、 13:00~14:00の予約を入れることはできるか? 普通は「できる」のだ。 運用上は、ぎりぎりまで会議室を使っていたら、 事前のセッティングとか、 参加者の出入りができなくなってしまうから、 例えば14:00まで会議室を予約した場合に、 13:55 には退出すること、というような規則を定めておいたり、 あるいは、暗黙の了解で、そのように運用されているものである。 まあ簡単にいえば、 予約した時間内で片付けも含めて全てのことを終わらせてくれ、 という話に過ぎない。

この処理を Range で実装することを考えてみよう。 もし両端を含むような Range を使って判定したら、 13:00~14:00と、14:00~15:00 は、 14:00 という一瞬を共有しているから、 overlapsRange は true を返すことになる。 実際、次のようなテストケースを実行すると、green bar が表示される。

---- List 3 (端点を含むかどうかの確認) ----

        IntRange range1 = new IntRange(10, 20);
        IntRange range2 = new IntRange(20, 30);
        assertTrue(range1.overlapsRange(range2));

---- List 3 end ----

唐突に話は逸れる。 range1、range2 という名前は妥当だろうか? これらが Range と関連があるかもしれない、 という想像のできる人は多いだろう。 その意味では最初の関門はクリアしている。 しかし、range10to20、のような名前の方がいいのではないか。 内容を的確に表現するという意味で、 さらにポイントが高いかもしれない。 ただ、ちょっと気になるのは可読性である。 この場合、確実に何をテストしたいのか示すには、 次のようなテストケースの方がいいかもしれない。

---- List 4 (意図がより明白なテストケース) ----

        assertTrue("", new IntRange(10, 20).overlapsRange(new IntRange(20, 30)));

---- List 4 end ----

これなら意図は明確である。 もちろん、 このように変数を使わずに書いた場合は、 他のテストを追加したくなっても、 同じオブジェクトを使いまわせないという制限が加わるから、 どちらがいいかというと、 ケースバイケースではないかと思う。

いずれにしても、 10~20と20~30を、 IntRange を使って処理すると、 この2つが overlap するという判定になることが分かった。 では、これが overlap しないと判定するような IntRange を使いたいとすれば、 どうすればいいか?

一つの考え方として、 整数の離散性を使うという方法はある。 つまり、10~20歳じゃなくて、10~19歳と20~29歳、 のように区切って、両端を含んでも重ならないようにすればいいのだ。 会議室予約の件なら、 13:00~13:59と、14:00~15:59、 のようになる。 もちろん、これは一般常識とはちょっとずれているから、 内部処理としてはこのように実装して、 画面表示上は、終了の時刻は1分を加算した方がいい。

とはいえ、 どうにもこれはエレガントではないような気がするし、 整数でない値を使いたいとなると、たちどころに困ってしまう。

(つづく)

  

(2006-05-21 Web に公開)
連載先募集中【謎】

(C) Phinloda 2006 All rights reserved.