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 とは記法以外に異なる点があることに注意
2012-02-28 修正
自分の書いた文章もまとめられないとは
- 空の Range にならないのは (from..to) の場合だけ