半開区間の探索

連続した半開区間があるとき、与えられた整数がどの半開区間に属するかを知りたい、なんて応用があって。

たとえば [0,30), [30, 50), [50, 80), [80, 100) とのような半開区間列があったとき 13 は [0, 30) に入るし、 50 は [50, 80) に入る。
この探索を Java コードでどう表現するか。

というので書いてみたのがこちら。:

class RangeFinder {
    public RangeFinder(long[] range) {
        assert range != null && range.length > 1 : "range は非 null で、要素を少なくともふたつ含む。";
        final int length = range.length - 1;
        index = new Range[length];
        for (int i = 0; i < length; ++i) {
            index[i] = new Range(range[i], range[i + 1]);
        }
    }

    public int find(final long value) {
        return Arrays.binarySearch(index, value);
    }

    private final Range[] index;

    private static final class Range implements Comparable<Long> {
        private final long beg;
        private final long end;

        public Range(long begin, long end) {
            this.beg = begin;
            this.end = end;
        }

        @Override
        public int compareTo(Long other) {
            final long value = other;
            if (value < beg) {
                return 1;
            }
            if (value < end) {
                return 0;
            }
            /* else */{
                return -1;
            }
        }
    }
}

こんなテストコードが動く。:

@Test
public void 所属する半開区間の探索() {
    // [0,10),[10,20),[20,30),[30,40),[40,50),[50,60),[60,70),[70,80),[80,90),[90,100)
    final long[] range = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };

    final RangeFinder finder = new RangeFinder(range);
    assertEquals(-1, finder.find(-1));
    for (int i = 0; i < 100; ++i) {
        assertEquals(i / 10, finder.find(i));
    }
    assertEquals(-11, finder.find(100));
}

binarySearch を使っているので、与える範囲の数列は昇順で並んでいることが前提。

もともとは…

もともとは binarySearch は値の探索には使えるけれど、連続した区間の探索に使うにはどうすればいいかな? というのがはじまりで。
long 配列があるんだったら、それを参照する形で流用できないものかと考えたのが次のコード。:

class OriginalRangeFinder {
    public OriginalRangeFinder(long[] range) {
        final int length = range.length - 1;
        this.range = range;
        index = new Index[length];
        for (int i = 0; i < length; ++i) {
            index[i] = new Index(i);
        }
    }

    public int find(final long value) {
        return Arrays.binarySearch(index, value);
    }

    private final long[] range;
    private final Index[] index;

    private final class Index implements Comparable<Long> {
        final int offset;

        public Index(int offset) {
            this.offset = offset;
        }

        @Override
        public int compareTo(Long other) {
            final long value = other;
            if (value < range[offset]) {
                return 1;
            }
            if (value < range[offset + 1]) {
                return 0;
            }
            /* else */{
                return -1;
            }
        }
    }
}

でも long と Index の配列を二本もたないほうが、そして Index クラスで親の range フィールドを共有しないほうが、わかりやすいだろうなというので冒頭に掲げたコードになりました。