倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

List 風の getAt() を更にしつこくやるよ

前回、GDK が List インターフェースに追加している getAt() メソッドの挙動をちょっと詳しく見てみました。 今回は、もしそれを実装するとどんなコードになるかを見ていきます。 実装に興味ない方はスルーよろしく。 あと、実際の GDK のソースコードは参照してませんので変なコードになってたり微妙に挙動が違ったりするかもです。 まぁ、拙者のプログラミング練習回でござる。

具体的なコードの方がいいかと思うので、java.nio.file.Path インターフェースに部分パスを返す getAt() メソッドを追加するカテゴリを作るってのを目標にしましょう。 Path インターフェースについてはこちらなどを参照。 今回実装で使うのは

  • Path#getName(int index) ・・・ index で指定された位置の部分パスを返す
  • Path#getNameCount() ・・・ 部分パスのサイズを返す

です。 具体的なコードで書いてみると

import java.nio.file.Path

def path = Paths.get('src/test/groovy/org/waman/gluino/nio')
assert path.getName(2) == Paths.get('groovy')
assert path.getNameCount() == 7

といった感じです。 カテゴリによって追加する getAt() メソッドのシグニチャ

  • getAt(int) : Path
  • getAt(EmptyRange) : List<Path>
  • getAt(IntRange) : List<Path>
  • getAt(List<Integer>) : List<Path>

の4つとします。 List インターフェースには getAt(IntRange) ではなく getAt(Range) が追加されていますが、まぁ IntRange で充分でしょう。

getAt(int) メソッド

getAt(int) は引数で指定された位置の部分パスを返す点は getName(int) と同じですが、

  • サイズ(getNameCount() で返される)を超えた場合に null を返す
  • 負の引数の場合に末尾から数えた位置の部分パスを返す

という追加の挙動があります。 まぁ、実装は素直に条件分岐でいいじゃない

    public static Path getAt(Path path, int i){
        if(i < 0)
            return path.getName(i + path.getNameCount());
        else if(i < path.getNameCount())
            return path.getName(i);
        else
            return null;
    }

カテゴリ・クラスを作る目的なので、第1引数が Path オブジェクトになってます。 負の引数の場合は引数にサイズ(getNameCount() の返す値)を加えれば OK。 これを加えてもまだ負の場合は例外を投げますが、これは List#getAt(int) も同じです。 同じことを Groovy で書くと

    static Path getAt(Path path, int i){
        switch(i){
            case 0..<(path.nameCount):
                return path.getName(i)
            case { it < 0 }:
                return path.getName(i + path.nameCount)
            default:
                return null
        }
    }

条件分岐が多いときは switch 分の方が分かりやすい気がするけど、今の場合あんまり変わらんかな。 まぁ、やってることは同じっすね。

getAt(EmptyRange) メソッド

getAt(EmptyRange) は空リストを返すだけです。 Java では

import java.util.Collections;
import groovy.lang.EmptyRange
    
    public static List<Path> getAt(Path path, EmptyRange range){
        return Collections.emptyList();
    }

Groovy では

    static List<Path> getAt(Path path, EmptyRange range){
        return [];
    }

って感じですね。

getAt(IntRange) メソッド

さて、一番面倒そうな挙動をするのがこの getAt(IntRange)。 そもそも IntRange のメソッドをそんなに使ったことがないので「えっ、そなの?」っていう挙動もありーのでいろいろハマってました・・・ まずは IntRange の必要な知識から。

IntRange のプロパティは from と to ががあり、プリミティブの int 値が必要なら fromInt, toInt によって取得できます。 で、ややこしい(と拙者が思う)のは from, to はそれぞれ境界のうち小さい方、大きい方の値がセットされているというところ。 これは、逆順の範囲を作ったときにちょっと戸惑うと思うんだけど・・・

def ascend = 0..10
assert ascend.from == 0    // これはまぁいいでしょう
assert ascend.to == 10
assert !ascend.isReverse()

def descend = 10..0
assert descend.from == 0    // え、そっちなの?
assert descend.to == 10
assert descend.isReverse()

ってことで、範囲が正順か逆順かは isReverse() メソッドで確かめる必要があるようです。 この辺りを踏まえて Java で getAt(IntRange) を実装すると

import java.util.*;
import groovy.lang.IntRange;

    public static List<Path> getAt(Path path, IntRange range){
        // (1)
        int left  = !range.isReverse() ? range.getFromInt() : range.getToInt();
        int right = !range.isReverse() ? range.getToInt()   : range.getFromInt();
        
        // (2)
        left  = left  >= 0 ? left : left  + path.getNameCount();
        right = right >= 0 ? right: right + path.getNameCount();
        
        // (3)
        if(left == right){
            return Collections.singletonList(path.getName(left));
        }else if(left < right){
            List<Path> result = new ArrayList<>(right - left + 1);
            for(int i = left; i <= right; i++)
                result.add(path.getName(i));
            return result;
        }else{
            List<Path> result = new ArrayList<>(left - right + 1);
            for(int i = left; i >= right; i--)
                result.add(path.getName(i));
            return result;
        }
    }
  • (1) left は from とは違って、範囲をリテラルで書いたときに正順・逆順どちらでも左側の値を参照します。 つまり、0..10 のとき left は0、10..0 のとき left は10となります。 right についても同じ。
  • (2) では範囲の境界が負の場合に適切な値(パスのサイズを加えたもの)にしています。 あとで getAt(int) で負のインデックスを処理しようとすると上手くいかないようなので、ここで変えてます
  • (3) 適切な値に変えられた left, right の大小関係で返り値のリストを構築

まぁ、実装の詳細はともかく、IntRange の使い方に対して理解が深まってよかったかなぁと。 ちなみに、引数の範囲を IntRange ではなく ObjectRange にした場合、範囲が Integer (に変換できる型)かどうかの処理や、引数が EmptyRange の場合の処理が必要*1。 ちなみに Groovy で実装する場合、パフォーマンス的にどうかは分かりませんが、以下のようにすると(おそらく)完全に List#getAt(IntRange) と同じ挙動になります:

    static List<Path> getAt(Path path, IntRange range){
        return (0..<(path.nameCount))[range].collect{ path.getName(it) }
    }

なんたって要素を取得するインデックスを List#getAt(IntRange) 自体で取得してるんだもんっ!

getAt(List) メソッド

getAt(List) は getAt(IntRange) に比べると素直に実装できます。 インデックスのリストに格納されている要素を回して Path から部分パスを取得するだけです。 Java で実装した場合はこんな感じ:

    public static List<Path> getAt(Path path, List<Integer> indices){
        List<Path> result = new ArrayList<>(indices.size());
        for(int i : indices)
            result.add(getAt(path, i));
        return result;
    }

Groovy でも同じようなコードを書けますが、getAt(IntRange) の最後にやったような方法でも書けます:

    public static List<Path> getAt(Path path, List<Integer> indices){
        return (0..<(path.nameCount))[indices].collect{ path.getName(it) }
        // もしくは getAt(int) を使って
        // return indices.collect{ path.getAt(it) }
    }

そういえば実装ばっかりかいて、実行コード、テストコード的なものを書いてませんでしたが、こちらに Spock 的なテストを書いてみました。

今回は実装の話でもあり、いつも以上に自己満足な記事になってしまいましたな(汗) IntRange の使い方あたりはちょっと役立つこともあるかも。

プログラミングGROOVY

プログラミングGROOVY

*1:Groovy ではオブジェクトのActual type によって呼び出すメソッドを動的に選択するので EmptyRange オブジェクトを getAt() メソッドに渡すと getAt(EmptyRange) メソッドが呼び出されるのでこの処理は必要ありませんが、もし Java コードからこのメソッドを使うことがあった場合、型付けの仕方によっては getAt(Range) メソッドに EmptyRange オブジェクトが渡されることがあります。 まぁ、カテゴリ・クラスを Java コードから使うことがあるのかどうか分かりませんが、ユーティリティ・クラスとして使えなくもないので、念には念を。