Groovy の Range

s = "abc"
assert "abc" == s[0..<s.size()]
assert "abc" == s[0..-1]
assert "ab"  == s[0..-2]
assert "a"   == s[0..<-1]

最後の動作に驚いたので Groovy の Range を調査する。

Range のプロパティ

class MyRange extends AbstractList /* implements Range */ {
  int     from, to
  boolean reverse

  MyRange(int from, int to) {
    this.reverse = from > to
    this.from = reverse ? to : from
    this.to   = to      ? from : to
  }

  int size() {
    return to - from + 1
  }

  Object get(int index) {
    if (index < 0 || size() < index) throw new IndexOutOfBoundsException()
    return reverse ? to - index : index + from
  }

  String toString() {
    return reverse ? "" + to + ".." + from : "" + from + ".." + to;
  }
}
Rage のプロパティは from, to, reverse の 3つ
irb(main):001:0> (1..3).end
=> 3
irb(main):002:0> (1...3).end
=> 3
irb(main):003:0> (1...3).exclude_end?
=> true

Ruby と違い to が範囲に含まれるかという情報は保持していない。

groovy:000> (1..3).to
===> 3
groovy:000> (1..<3).to
===> 2
reverse == true でも from <= end

Ruby は begin <= x <= end な x を表現しているので reverse な場合は empty である。

irb(main):004:0> (3..1).begin
=> 3
irb(main):005:0> (3..1).end
=> 1
irb(main):006:0> (3..1).to_a
=> []

Groovy では、reverse な場合は from と to が入れ替わる。

groovy:000> (3..1).from
===> 1
groovy:000> (3..1).to
===> 3
groovy:000> (3..1).reverse
===> true
set, add, remove を呼び出すと UnsupportedException を throw する

AbstractList の set, add, remove を override していないため

Range インスタンスの生成

Range は ScriptBytecodeAdapter.createRange で生成される。
以下、IntRange の生成箇所を抜き出してコメントを加えた。

int ito = (Integer) to;
int ifrom = (Integer) from;
if (!inclusive) {
    // ..< の場合
    if (ifrom == ito) {
        return new EmptyRange((Comparable) from);
    }
    if (ifrom > ito) {
        ito++; // ObjectRange の場合は next()
    } else {
        ito--; // ObjectRange の場合は previous()
    }
}
return new IntRange(ifrom, ito);


とりあえずここで最初の謎は解けた。
reverse な場合は next() されるので index == 0 の要素だけ選択されたわけだ。

groovy:000> (0..<-1) == (0..0)
===> true


調べたら Groovy イン・アクションにも next() される例は載っていた。*1

log = ''
(9..<5).each { element ->
  log += element
}
assert log == '9876'

負の index 意味

int でも IntRange でも、負の index の処理は同じである。
DefaultGroovyMethods.normaliseIndex

protected static int normaliseIndex(int i, int size) {
    int temp = i;
    if (i < 0) {
        i += size;
    }
    if (i < 0) {
        throw new ArrayIndexOutOfBoundsException("Negative array index [" + temp + "] too large for array size " + size);
    }
    return i;
}

index < 0 であれば size が足されるだけだ。
しかし、その後の ArrayIndexOutOfBoundsException は曲者である。
空リストで -1 の場合は必ず例外になるからだ。

まとめ

  • from ..< to で reverse な場合は to.next() される
  • empty な可能性があれば、範囲の指定には 0 .. -1 ではなく 0 ..< a.size() を使う*2
  • index がマイナスの場合は size が足される
  • それでもマイナスの場合は ArrayIndexOutOfBoundsException が発生する
  • x .. -1 を x 以降全ての意味で使用すると、empty な場合に例外が発生するので注意する

2012-02-17 追記

自分で読み返してみてまとめがまとまっていない感じがしたので追記
Groovy の Range の4つのポイント

  • Range は List である
  • 空の Range は存在しない
    • 最低でも size は 1
  • from == to かつ右境界を含まない場合ときのみ空になる
    • reverse 状態があるので from > to は空にならない
assert (0..<0).empty
assert ('a'..<'a').empty
  • from > to のときの振る舞い
// from > to であれば reverse される
assert (9..0).from == 0
assert (9..0).to   == 9
 
// reverse 状態でも getAt は reverse 前と同じように振る舞う
assert (9..0)[3]  == 6
assert (9..0)[-1] == 0
 
// from > to かつ右境界を含まない場合は、右境界が next される
assert (9..<0) == (9..1)
  • Range で扱える型の条件
    • next と previous を実装している
    • java.lang.Comparable である


Ruby とは記法以外に異なる点があることに注意

  • Ruby では Range は Enumerable であるが Array ではない
  • Ruby には空の Range が存在する
  • Ruby には Range の reverse 状態は存在しない
    • begin <= x <= end
    • begin <= x < end

2012-02-28 修正

自分の書いた文章もまとめられないとは

  • 空の Range にならないのは (from..to) の場合だけ

*1:今まで気が付かなかった

*2:飛躍しすぎているので修正