倭マン's BLOG

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

Groovy でグループを使った正規表現マッチを試す

なんとなく今まで正規表現を回避してたんだけど、定型文字列を String のメソッドだけで解析するの面倒すぎるので、Groovy パワーであれこれ試してみました。 Groovy 使うと、グループを使った正規表現でマッチしたグループの内容を List の要素アクセスのように取得できるので楽そう。

正規表現と Matcher の基本
まずは簡単なサンプルで動作を確認してみましょう。 対象とする文字列は

def method0(String s)

のような Groovy のメソッド宣言(ただしメソッド本体無し)。 この文字列に対して正規表現でメソッド名と引数の型、名前を取得します。 正規表現をあまり複雑にしないために、空白文字の部分は1つの空白のみ許すことにします(その他、余計な空白はないとします)。 あと、メソッド名などの最初に数字が来てもマッチします。

では実際にサンプルを書いてみましょう。 Groovy では正規表現を表す文字列は /.../ で書くとエスケープが少なく済んで楽ですね。

// 対象文字列
def s0 = 'def method0(String s)'

// 正規表現
def ARG = /(\w+) (\w+)/    // 引数部分の正規表現。 グループ2つ
def regex0 = /def (\w+)\($ARG\)/

// マッチの実行
def matcher0 = s0 =~ regex0    // =~ は正規表現検索演算子

// Matcher のあれこれ
assert matcher0 instanceof java.util.regex.Matcher

assert matcher0.hasGroup()
assert matcher0.groupCount() == 3  // 正規表現に含まれるグループの数

assert matcher0.size() == 1    // マッチした部分の数を取得
assert matcher0.size() == matcher0.count  // size() と count は同じ

assert matcher0[0] instanceof List    // Matcer から要素風に取得したオブジェクトは List
assert matcher0[0] == ['def method0(String s)', 'method0', 'String', 's']
    // 1つ目の要素はマッチした文字列全体、後の要素は各グループにマッチした文字列

GDK の機能によって Matcher に matcher0[0] のような方法(getAt メソッド)でマッチ結果を取得することができます。 その取得結果は List オブジェクトで、その要素は

  • 最初の要素はマッチした文字列全体
  • それ以降の要素はグループにマッチした文字列

となっています。 ここでの正規表現は

def (\w+)\((\w+) (\w+)\)

ですが*1、これを「def (A)\((B) (C)\)」としたとき、matcher0[0] で返される List には

  • 0. def (A)\((B) (C)\)
  • 1. (A)
  • 2. (B)
  • 3. (C)

にマッチする文字列(グループ化の丸括弧 () は含まれません)が返されます。 グループが入れ子になっていないと簡単。 なってても別に難しくないけど。 で、今の場合には以下の文字列が割り当てられます:

  • 0. def (A)\((B) (C)\) ・・・ def method0(String s)
  • 1. (A) ・・・ method0
  • 2. (B) ・・・ String
  • 3. (C) ・・・ s

最初がマッチした文字列全体だって以外は特に注意が必要なところはないですかね。

複数のマッチがある場合
上記では「matcher0[0]」でマッチ結果の List を取得できることを見ましたが、例えばメソッド名を取得したい場合には「matcher0[0][1]」のように2重配列であるかのようにアクセスする必要があります。 先ほどの例ではこの1つ目の [0] の必要性がよく分からないので、これが意味のある場合のサンプルを見ておきましょう。 正規表現は先ほどと同じものを使っています:

// 対象文字列
def s1 = '''
  def method1(String s)
  def method2(int i)
  def method3(double d)
'''

// マッチの実行(正規表現はさっきといっしょ)
def matcher1 = s1 =~ regex0

// マッチ結果
assert matcher1.size() == 3
assert matcher1[0] == ['def method1(String s)', 'method1', 'String', 's']
assert matcher1[1] == ['def method2(int i)',    'method2', 'int',    'i']
assert matcher1[2] == ['def method3(double d)', 'method3', 'double', 'd']

Groovy の正規表現検索演算子「=~」は対象文字列にマッチした部分が複数あってもきちんと抜き出してくれるんですね。 ちなみに正規表現マッチ演算子「==~」は対象文字列全体が正規表現にマッチしているかどうかを検証します。

Matcher オブジェクトの各要素(matcher1[0] などで返される List オブジェクト)がどのような要素からなるかは、先ほどのサンプルと同じです。

グループに数量子をつけると・・・
さて、対象文字列のメソッド宣言が引数1つというのは融通が利かないので、複数の引数でもいいように拡張してみましょう。 引数がない場合もできますが、正規表現が複雑になるのでここでは割愛。 で、2つ目以降の引数は数量子 * を使って書きましょう:

// 対象文字列
def s2 = '''
  def method4(String s, int i, double d)
  def method5(long l)
'''

// 正規表現(ARG は上記のものと同じ)
def regex2 = /def (\w+)\($ARG(, $ARG)*\)/

// マッチの実行
def matcher2 = s2 =~ regex2

// マッチ結果
assert matcher2[0] == ['def method4(String s, int i, double d)', 'method4',
                       'String', 's', ', double d', 'double', 'd']
assert matcher2[1] == ['def method5(long l)', 'method5', 'long', 'l', null, null, null]

さて、マッチ結果を見てみると正規表現初心者には少々腑に落ちないところがいくつか。

  • matcher2[0] に引数「int i」に関連する部分が抜け落ちている
  • 数量子「*」を付けるための丸括弧 () がグループにされて「, double d」が結果に入れられている(まぁ、仕方ない?)
  • matcher2[1] にやたら null がある

最初、正規表現の書き方がおかしいのかと悩んでたんだけど、どうもこれが(Java 正規表現の)通常の動作のようで。 これがどういうことかと言うと、今の正規表現

def (\w+)\((\w+) (\w+)(, (\w+) (\w+))*\)

を「def (A)\((B) (C)(, (D) (E))*\)」とすると、定義されるグループは

  • 0. def (A)\((B) (C)(, (D) (E))*\)
  • 1. (A)
  • 2. (B)
  • 3. (C)
  • 4. (, (D) (E))
  • 5. (D)
  • 6. (E)

となります。 ちょっと直感とのズレがあるのはグループ4で、数量子「*」が付いていても(というか数量子が付いているかどうかに関係なく)グループは1つになり、マッチする文字列は1つしか返されないということです。 返される文字列はマッチした最後の文字列となります。 もう少し具体的に見てみると(0番目のマッチした文字列全体は長くなるので省略して)

(A) (B) (C) (, (D) (E)) (D) (E)
1行目1つ目 method4 String s , int i int i
1行目2つ目 , double d double d
1行目の結果 method4 String s , double d double d
2行目 method5 long l
2行目の結果 method5 long l null null null

のように int 関連の引数が double のもので上書きされます。 グループはどんな数量子が付いても1つのグループってことですな。 この動作ってどう使ったらいいのかよくわからん。 そもそも groupCount() メソッド (Java) とか hasGroup() メソッド (GDK) とかは Pattern オブジェクトに定義した方がいいんじゃないか?って気もするし。

引数の個数が分からないなら別途処理するしか・・・
では、引数の部分が複数個ある場合にはどうしたらいいのかと考えると、引数部分だけ抜き出して別途処理するしかなさそうですな。 ARG にあたる部分のグループ化を解いて、引数部分を1つのグループにしてマッチさせましょう。 数量子を付けるために丸括弧 () を付けざるを得ないところがありますが、「(?:」によってグループと認識されないようにしておきましょう*2

//対象文字列
def s4 = 'def method6(String s, int i, double d)'

// 正規表現
def ARG = /\w+ \w+/    // 最初のサンプルと変わってます
def ARGS = /$ARG(?:, $ARG)*/    // (?: でグループにしない括弧
def regex3 = /def (\w+)\(($ARGS)\)/

// マッチの実行
def matcher3 = s3 =~ regex3
assert matcher3[0] == ['def method6(String s, int i, double d)', 'method6',
                       'String s, int i, double d']
assert matcher3[0][2].split(/, /) == ['String s', 'int i', 'double d']
    // String#split(regex) で引数部分を分離

引数部分全体(部分なのに全体w)を取得できたら、String#split() メソッドでさらに分離すれば1つ1つの引数部分が得られます。 この split メソッドの引数は正規表現として扱われるので、/,\s+/ などを渡せば複数の空白文字で区切られている場合でもキチンと処理してくれます。 めでたしめでたし。

上記と同じことを Java だけでやろうと思ったんだけど、思いの外大変そう。

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (85件) を見る
正規表現クックブック

正規表現クックブック

*1:エスケープされた「\(」と「\)」は文字列としての丸括弧でグループ化とは無関係。

*2:グループと認識されないようにする丸括弧にもいくつか種類があるようだけど、イマイチ違いがわからん。

Windows 厨でも gvm したい ~Cygwin 編~

別に Windows 厨というほど使い倒してるワケではないんですが、Windows でも GVM (Groovy enVironment Manager ) を使いたいなぁと思って試してみました。 GVM は Groovy 関連のツールを簡単にダウンロード & インストールできるツールです。 Groovy をはじめ

  • Gradle
  • vert.x
  • Groovyserv
  • Grails
  • Griffon

なども簡単にインストールできるのでとっても便利、らしいです。 ただ、bash で書かれてるので Windows人にはちょっと敷居が高い感が否めないところ。 bash なら Windows 上で使うには Cygwin をインストールして使うのが王道だろうということで、今回はそれを試してみます。

  1. Cygwin + curl のインストール
  2. GVM のインストール
  3. Cygwin Terminal で gvm
  4. コマンド・プロンプトで gvm?

Cygwin + curl のインストール

Cygwin 自体のインストール方法は、以前の記事『Cygwin + wget インストール』で書いたのでここでは省略。 2度も書きたくない面倒さw ただし、GVM をインストールするためには curl コマンドを使えるようにしないといけないので、前記記事で wget コマンドを選択してインストールしたように、「Select Packages」ダイアログの箇所で「Net」カテゴリにある「curl」にチェックを入れて選択してください。
f:id:waman:20140626060715p:plain

Cygwin はインストールしているけど curl コマンドはインストールしていない、という場合も上記記事と同様に setup.exe から設定を始めます。 Cygwin + curl さえインストールできれば勝ったも同然。

以下、Cygwin をインストールしたディレクトリを 《Cygwin Root》 とします。

GVM のインストール

Cygwin がインストールできたら、デスクトップにできているであろうアイコン(もしくはスタートメニュー)から Cygwin Terminal を起動します。
f:id:waman:20140626062813p:plain

起動が完了したら、curl コマンドによって GVM をインストールします:

curl -s get.gvmtool.net | bash

インストールがうまくいけば、以下のような「GVM」のアスキーアートが表示されるかと思います(下図は既に GVM がインストール済みの場合に curl コマンドを実行した結果ですが)

f:id:waman:20140626062418p:plain

最初インストールするときは何か聞かれたかも。 聞かれなかったかも。 まぁ、curl コマンドさえ使えれば1行のコマンドでインストール完了します。 簡単だ。

ちなみに GVM は

Cygwin Root》/home/《ユーザー名》/.gvm

にインストールされるようですね。 GVM が(自動)ダウンロードするアーカイブなどもこのディレクトリ下に置かれるもよう。

Cygwin Terminal で gvm

では、実際に GVM を使ってみましょう。 まずは Cygwin Terminal にて。 Groovy をダウンロード & インストールしてみます。 バージョンは 2.4.0-beta-1 で。 Cygwin Terminal 上で以下のコマンドを実行します:

gvm install groovy 2.4.0-beta-1

ダウンロードが完了したら、(Groovy の)デフォルトとしてこのバージョンを使うか聞いてくるので、y/n で答えてあげます。 実際にダウンロード & インストールした Groovy は groovy コマンドで実行できます。 例えば

groovy -v

として Groovy のバージョンを確認してみるといいでしょう:

f:id:waman:20140626065912p:plain

指定したバージョンの Groovy が使われてれば OK です。

作業フォルダ
Cygwin Terminal を使う際に少々気にくわないのは作業ディレクトリが「《Cygwin Root》/home/《ユーザー名》」に半強制されるところ。 まぁ、それを受け入れてこのディレクトリ下にプロジェクトを作成していっても別にいいんですが、Windows のデフォルトのユーザー・ディレクトリなどで作業したいこともあるかと思います。 Windows のコマンド・プロンプトなら

「コマンド・プロンプト」アイコン → ポップアップ・メニュー(右クリック) → プロパティ(R) → 「ショートカット」タブ → 作業フォルダー(S)

に起動したいディレクトリへのパスを設定すれば、そのディレクトリでコマンド・プロンプトを開けます。 一方、Cygwin Terminal はこの設定が利かないようですが、もちろん別の箇所でこの設定はできます。 あんまり Cygwin (というか bash)に詳しくないので作法がよく分かりませんが、

  • Cygwin Root》/home/《ユーザー名》/.bashrc

というファイルを開いて、最後の方に

cd c:Documents\ and\ settings/《ユーザー名》  # この行を追加

#THIS MUST BE AT THE END OF THE FILE FOR GVM TO WORK!!!
[[ -s "/home/higashida/.gvm/bin/gvm-init.sh" ]] && source "/home/higashida/.gvm/bin/gvm-init.sh"

のように cd コマンドで移動しておけばいいんじゃないかと。 最後2行は GVM が .bashrc ファイルの最後に追加した行なので書く必要なし。 コメントによると、この行を最後にしとかないと GVM が動かんぞ!!!とか言ってるので、その上に追加しておきました。

コマンド・プロンプトで gvm

Cygwin をインストールして環境変数 PATH に「Cygwin Root》\bin」を追加しておくと、コマンド・プロンプトや PowerShell から Cygwin (というか bash のコマンド)が使えます。 ただし、ここで使えるコマンドは上記 bin ディレクトリにある exe ファイルに対応するコマンドだけなので、シェルスクリプトとして書かれている gvm コマンドは実行できません。 なので、bash コマンドを使ってこのシェルスクリプトを呼び出す必要があります:

bash --login -i -c "gvm install groovy 2.4.0-beta-1"

--login はログインする、-i はインターラクティブ・モード、-c は後に続くコマンドを実行、らしいです。 bash を使って作業を続けたい場合は

bash --login -i

$ gvm groovy install 2.4.0-beta-1

(最終行の行頭にある「$」記号は入力の必要なし)のようにすることもできます。

とまぁ、Cygwin を使って Windows 上で GVM を使う方法を試してみました。 Cygwin さえインストールすれば GVM 自体のインストールは簡単だと思いますが、Cygwin さんのハードルの高さたるや・・・ とか思ってたら、1年以上前に

なんていう記事があったので、次はこの内容をそのまま試してみようかなと。 あんまりそのまま過ぎるとアレなので、この記事で使われている mingw ってツール経由で Cygwin をインストールしてみる、なんてのも試せるといいなぁ。 いつになるかは分かりませんが(笑)

【改訂新版】 Windows PowerShell ポケットリファレンス

【改訂新版】 Windows PowerShell ポケットリファレンス

Windows PowerShellクックブック

Windows PowerShellクックブック

Spock ことはじめ ~without IDE 編~

Java/Groovy のテスティング・フレームワーク Spock の使い方をあれこれ調べてみました。 まぁ、Gradle 使えるならそれに越したことはないですが、なんとなく Maven3 やコマンドラインからの使用方法も試してみました。

どうも Maven3 から GMaven が Groovy コードのコンパイルをサポートしなくなったようで(Groovy スクリプトの実行などに機能を絞ったもよう)、Spock のドキュメントに書いてある方法では Maven3 上で Spock を使えなくなってます。 Maven3 での Groovy コードのコンパイルGroovy Eclipse compiler プラグインというのを使って行うようです。 このプラグインは Maven3 の Java コンパイラを Groovy もコンパイルできるものに置き換えるというアプローチの Groovy サポートなようです。 まぁ、Maven3 上で Spock を使うってのにどの程度のニーズがあるか分かりませんが。

それ以上に使うかどうか分かりませんが、Gradle や Maven3 のようなビルドツール(プロジェクト管理ツール)を使わずに groovy コマンドで Spock テストを行うことも出来ます。

他に Apache Ant を使ったビルド方法もあるようですが、build.xml を読むのが面倒だったのでパス。 ご興味のある方はこちらを参照。

目次

参考

Gradle

まずは王道の Gradle による Spock テスト。 プロジェクトのディレクトリ構造が以下のようになっているとします:

  • ${project.rootDir}
    • build.gradle
    • src
      • main
      • test
        • groovy
          • my
            • test
              • HelloSpock.groovy

ファイルは build.gradle と HelloSpock.groovy のみ。 build.gradle はプロジェクトのルートディレクトリに、HelloSpock.groovy は my.test パッケージに配置しています。 まぁ、普通の Gradle プロジェクトですね。 プロジェクトのテストに Spock を使うには、build.gradle に以下のような設定を書きます(ちょっと余計な設定を入れてますが):

// build.gradle
apply plugin: "groovy"

group = "my.test"
version = "1.0-SNAPSHOT"
sourceCompatibility = 1.8
targetCompatibility = 1.8
tasks.withType(Compile){ options.encoding = "UTF-8" }

repositories.mavenCentral()

dependencies{
    compile "org.codehaus.groovy:groovy-all:2.3.0-rc-1"
    testCompile "org.spockframework:spock-core:0.7-groovy-2.0"
}

task wrapper(type: Wrapper) {
    gradleVersion = "1.12"
}

大事なのは「apply plugin : "groovy"」と dependency ノード下の依存性の設定です。 Gradle のちょっと前のバージョンから、Groovy の依存性は groovy ではなく compile によって設定するようになりましたね*1

次は Spock によるテストコード HelloSpock.groovy。 Spock wikiHelloSpock」から拝借しました(パッケージ宣言だけ追加しています):

// HelloSpock.groovy
package my.test

import spock.lang.*

class HelloSpock extends Specification{
    def "length of Spock's and his friends' names"(){
        expect:
        name.size() == length

        where:
        name     | length
        "Spock"  | 5
        "Kirk"   | 4
        "Scotty" | 6
    }
}

これで準備 OK。 次はテストの実行。

テストの実行
上記のようにプロジェクトが用意できたら、次はテストの実行。 プロジェクトのルートディレクトリで以下のようにコマンドを実行します*2

gradle test

実行結果は

  • ${project.rootDir}/build/reports/tests/index.html

から確認できます。 コマンドライン上で確認できないのは少々面倒。 まぁ、失敗が多いとブラウザで構造的に閲覧できないと手に負えなくなりますがね。

テストのフィルタリング
Gradle の最後は、1つもしくは一部のテストを指定して実行する方法。 Gradle User Guide 「The Java Plugin : Test」 の Test filtering の箇所を参照。 「--tests」オプションを使用して、クラスやメソッド (feature) を完全修飾名で指定したり、ワイルドカード「*」で適合するものを指定したりできます:

  • gradle test --tests org.gradle.SomeTest.someSpecificFeature
  • gradle test --tests *SomeTest.someSpecificFeature
  • gradle test --tests *SomeSpecificTest
  • gradle test --tests all.in.specific.package*
  • gradle test --tests *IntegTest
  • gradle test --tests *IntegTest*ui*
  • gradle someTestTask --tests *UiTest someOtherTestTask --tests *WebTest*ui

たとえば my.test.HelloSpec クラスに定義されたテストを全て実行するためには、以下のコマンドを実行します:

gradle test --tests my.test.HelloSpec

まぁ、ただ個別にテストを実行するには IDE を使う方が楽ちん。 IDE 上で Spock を使う方法は後日に。 って、普通に JUnit のように使えるようですけど。

Maven 3

次は Maven3 上で Spock を使う方法。 試した Maven3 のバージョンは 3.2.1 です。 かなり久し振りに Maven 使った(というかインストールした)。 プロジェクト構造は Gradle と同じ(Gradle が Maven2/3 と同じという方が正しいかな?):

  • ${project.rootDir}
    • build.gradle
    • src
      • main
      • test
        • groovy
          • my
            • test
              • HelloSpockTest.groovy

Spock テストのファイルを HelloSpockTest.groovy に変更してるのに注意。 もちろんクラス名も変更してます。 どうも、Maven3 に(というか、テストを実行している Surefire プラグインに)テストであることを認識してもらうためにはクラス名が Test で終わらないといけないようです。 こちらにテストとして含めるファイル名(クラス名)の指定方法が書かれてますが、試してもうまくいかなかったのでスルーします。 Gradle の build.gradle に対応する pom.xml は以下のようにします(XML 助長過ぎる・・・):

<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
         
  <modelVersion>4.0.0</modelVersion>
  <groupId>my.test</groupId>
  <artifactId>spock-test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>Spock Test</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <compilerId>groovy-eclipse-compiler</compilerId>
          <source>1.7</source>
          <target>1.7</target>
          <!--verbose>true</verbose-->
        </configuration>

        <dependencies>
          <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-compiler</artifactId>
            <version>2.8.0-01</version>
          </dependency>

          <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>2.1.8-01</version>
          </dependency>
        </dependencies>
      </plugin>

      <plugin>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-eclipse-compiler</artifactId>
        <version>2.8.0-01</version>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.1.8</version>
    </dependency>

    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>0.7-groovy-2.0</version>
      <scope>test</scope>
    </dependency>    
  </dependencies>
</project>
  • いまのところ、動かせる Groovy のバージョンは 2.1.8 のようです。 groovy-eclipse-batch プラグインが対応していない Groovy のバージョンはダメなよう(試してないけど)。
  • 詳細な情報を出力したい場合は、maven-compiler-plugin の設定の箇所の、コメントアウトしてある verbose 要素の部分のコメントを外します。
  • javaソースコードのバージョンを指定する source/target に 1.8 を指定すると、いまのところ例外が投げられるようです。 ラムダ式とか使ってなくても。

pom.xmlXML 地獄以外は Gradle の場合と対して変わりませんね。 テストの実行は簡単。 プロジェクトのルートディレクトリで以下のコマンドを実行します:

mvn test

テストのフィルタリングもできるようですが、まぁ、いいでしょう。

コマンドライン

最後はコマンドラインから Spock を使う方法。 Spock wiki にはコマンドライン上で Jar ファイルをクラスパスに指定してテストを実行する方法が書かれてますが、ここでは Grape によって Jar ファイルを自動ダウンロードする方法を見てみましょう。 実質的には同じですが。 Grape によって Spock を使うには、Spock テストのコードに、以下のような @Grab, @GrabExclude アノテーションを追加します:

// HelloSpock.groovy
@Grab("org.spockframework:spock-core:0.7-groovy-2.0")
@GrabExclude("org.codehaus.groovy:groovy-all")
import spock.lang.*

class HelloSpock extends Specification{

    def "length of Spock's and his friends' names"(){
        expect:
        name.size == length

        where:
        name     | length
        "spock"  | 5
        "Kirk"   | 4
        "Scotty" | 6
    }
}

2つのアノテーション以外は同じです。 @GrabExclude アノテーションは、groovy-all.jar のバージョン衝突を防ぐために付けておく必要があります。 テストの実行は普通の Groovy スクリプトの実行と同じです:

groovy HelloSpock
  • コマンドを実行するディレクトリ上にある Groovy コードは自動でクラスパスに含められます。 それ以外に使用するソースコードがある場合は、そのファイルがあるディレクトリを「-cp」オプションによってクラスパスに含める必要があります。
  • Grape を使わないなら「-cp」オプションで Spock Framework と JUnit の Jar ファイルをクラスパスに追加します。 JUnit はバージョンによっては他に Jar が必要だったかもしれません。 そのあたりの依存性解決が面倒なので Grape でやっておく方が無難かと。

とりあえず Spock を使うための環境整備はこんなものですかね。 通常の開発では IDE からテストを行うことの方が多いと思いますが、Spock テストは JUnit が動く IDE 上では同じように使えるので便利です。 後は Gradle のマルチプロジェクトで使う場合をもうちょっと試したいところですが、それらは次回以降に見ていきたいと思います。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

プログラミングGROOVY

プログラミングGROOVY

*1:これを testCompile にするとテストにのみ Groovy を使うようにできるのかな? とりあえずテストは機能しますがパッケージングしたりするのが面倒なので成果物が Groovy に依存しないかどうかは試してません。

*2:もしくは、一度「gradle wrapper」と実行して、その後に「gradlew test」と 'w' を付けて実行。

GDK に Java8 で導入された日時 API の拡張が追加されてたので試してみた

Java8 の目玉機能はラムダ式ですが、その次によく取り上げられるのが新しい日時 API である Date and Time API(JSR-310) でしょう。 ただ、この API はナカナカに取っつきにくいという非難の声が。 まぁ、日時の扱い自体が面倒なので仕方がないという擁護の声もありますが。

で、Java で扱いにくい API を似たような文法を保ちつつ扱い易くしよう!ってのが Groovy による JDK の拡張である GDK です。 この GDK に早くも新しい日時 API に対する拡張が追加されてたのでちょっくらイジってみました。 ただし、そもそもこの日時 API 自体にいまいち馴染んでいないので、GDK には関係なく単に JavaAPI を Groovy 上で動かしてるだけの箇所もあります。

この記事では日付 (Date) と時間 (Time) をともに扱う XxxxDateTime のみを扱い、昔ながらの java.util.Date に変換する箇所以外は、タイムゾーンを考慮しない LocalDateTime を見ていきます。 Date に変換する箇所では ZonedDateTime を扱います。

日時(~日付+時間) 日付 時間
タイムゾーンを考慮しない LocalDateTime LocalDate LocalTime
オフセット云々 OffsetDateTime - OffsetTime
タイムゾーンを考慮する ZonedDateTime - -

ちなみに使用するバージョンは、Java はもちろん8、Groovy は 4.1.2014 とします。

概要

  1. 日時の作成
  2. 文字列からの読み取り/への書き出し(パース/フォーマット)
  3. Date への/からの変換
  4. Groovy による演算子オーバーロード
  5. その他

参考

日時の作成

まずは日時の作成方法、つまり LocalDateTime オブジェクトの取得方法から見ていきましょう。 次節で見る文字列からの読み取りもよく使いますが、とりあえずそれ以外の方法のものを。 GDK は全く関係ありません。 次節以降のための準備です。 現在時刻は LocalDateTime#now() メソッドを使って取得します(コードは Groovy):

import java.time.*

def ldt1 = LocalDateTime.now()
println "It is $ldt1 now."
// 現在の日付と時刻 「It is 2014-04-01T01:23:45.678 now.」などと表示

日時の数値を指定してインスタンス生成するには LocalDateTime#of() メソッドを使います:

def ldt2 = LocalDateTime.of(2014, 4, 1, 1, 23, 45, 678000000)
assert( ldt2.toString() == '2014-04-01T01:23:45.678' )

def ldt3 = LocalDateTime.of(2014, Month.APRIL, 1, 1, 23, 45)
assert( ldt3.toString() == '2014-04-01T01:23:45' )

2つ目では月の指定に Month 定数 (Enum) を使い、ナノ秒部分を省略しました。 Enum 使えるの今どきw

次に、後で使う ZonedDateTime オブジェクトの取得方法を見てみましょう。 ZonedDateTime クラスは LocalDateTime にタイムゾーン(対応するクラスは java.time.ZoneId)の情報を付加したようなクラスです。 タイムゾーンとは 'Asia/Tokyo'*1 とか 'Greenwich' とか 'US/Hawaii' とかいうものです。  ZonedDateTime クラスの生成方法はいろいろあります。 LocalDateTime#atZone() メソッドを使うのあたりが、知らないと気づかなさそう:

//ZonedDateTime (≒ LocalDateTime + ZoneId) のインスタンス生成
// その1
def zdt1 = ZonedDateTime.now(ZoneId.systemDefault())

// その2
def zdt2 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())
assert( zdt1.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

// LocalDateTime オブジェクトを用意
def localDateTime = LocalDateTime.of(2014, 4, 1, 0, 0, 0)

// その3
def zdt3 = ZonedDateTime.of(localDateTime, ZoneId.systemDefault())

// その4
def zdt4 = localDateTime.atZone(ZoneId.systemDefault())

その他の方法は ZonedDateTime の JavaDoc でもご覧下さい。 どの方法でも ZoneId オブジェクトを何とかして取得しないといけないんですが、デフォルトのものを使うなら ZoneId#systemDefault() メソッドで取得できます。 その他のものは ZoneId#of() メソッドで取得できます。 このメソッドの引数にする利用可能な文字列一覧は ZoneId#getAvailableZoneIds() で取得できます:

// 利用可能な ZoneID (を表す文字列)の一覧を表示
ZoneId.availableZoneIds.sort{ it }.each{ println it }

文字列からの読み取り/への書き出し

次は文字列を読み取って LocalDateTime オブジェクトを構築したり、逆にフォーマットを指定して文字列に書き出す方法。 文字列から読み取るには LocalDateTime#parse() メソッドを使います:

def ldt4 = LocalDateTime.parse('2014-04-01T01:23:45')
assert( ldt4 == ldt3 )    // ldt3 は前節で作成したオブジェクト

読み取る文字列の形式は決まっていて、「yyyy-MM-ddTHH:mm:ss.nnnnnnnnn」のようにしないといけません(DateTimeFormatter#ofPattern() メソッドで他のパターンを使用可)。

  • d と H の間の T は文字
  • n はナノ秒で最大9桁
  • n 以外は桁を省略できない、つまり1時は「01」と2桁で書かないといけない
  • ナノ秒を省略したり(「yyyy-MM-ddTHH:mm:ss」)、ナノ秒と秒を省略したり(「yyyy-MM-ddTHH:mm」)できる。 分以上は省略不可っぽい

などの決まりがあります。 LocalDateTime オブジェクトの toString() メソッドはこの形式の文字列を返します。 別の形式の文字列に整形したい場合は LocalDateTime#format() メソッドと java.time.fomat.DateTimeFormatter クラスを使います:

import java.time.format.DateTimeFormatter as Formatter

assert( ldt4.format(Formatter.BASIC_ISO_DATE) == '20140401' )

独自フォーマットの DateTimeFormatter を作るのは結構大変っぽいので(「続・今日から始めるJava8 - JSR-310 Date and Time API」参照)、DateTimeFormatter の static 定数として定義されてるオブジェクトを使い回すのが無難。 ちなみに、頑張って独自の DateTimeFormatter オブジェクトを作っても、読み取りには使えなさそう DateTimeFormatter#ofPattern() メソッドを使うとできるようです。

GDK!
GDK は Date クラスに対して

def date = Date.parse('yyyy/MM/dd', '2014/04/01')
assert( date.format('yyyy/MM/dd') == '2014/04/01' )

のように、フォーマットとそれにマッチする文字列を指定して Date オブジェクトを読み取る parse() メソッドと、フォーマットを文字列で指定して書き出す format() メソッドが定義されていました。 GDK は LocalDateTime クラスにも同様のメソッド parse()/format() メソッドを追加しています:

// GDK!
// parse()
def ldt5 = LocalDateTime.parse('yyyy/MM/dd', '2014/04/01')
assert( ldt5.toString() == '2014-04-01T00:00')

// format()
assert( ldt5.format('yyyy/MM/dd') == '2014/04/01' )

Date への/からの変換

新しい日時 API で結構不評っぽいのが、昔ながらの Date オブジェクトへの変換が結構面倒だ、という点です。 まずは通常の Java API を使って変換する方法を見ていきましょう。 今まではタイムゾーンを気にしてませんでしたが、Date オブジェクトに変換するにはこれを扱う必要があるので、java.time.ZonedDateTime を考えます。 さて、まずはデフォルトの ZoneId を使うことにして ZonedDateTime と Date との変換を行ってみましょう。 この変換に際しては、

  • java.time.Instant オブジェクトを介して行う
  • java.util.Date クラスの from()/toInstant() メソッドを使う

という点に注意。 ZonedDateTime の APIjava.util.Date などの旧式のクラスに汚染されていません(作成者の意向らしいですが)。 ZonedDateTime から Date への変換は Date#from() メソッドを使います。 ZonedDateTime オブジェクトをいったん Instant オブジェクトに変換する必要があります:

def zdt1 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())

// ZonedDateTime -> Date
def date1 = Date.from(zdt1.toInstant())
assert( date1.toString() == 'Tue Apr 01 00:00:00 JST 2014' )

案外簡単。 次は逆に Date から ZonedDateTime へ変換する方法。 こちらは Date#toInstant() メソッドによって Instant オブジェクトを取得し、Instant#atZone() メソッドによって ZoneDateTime オブジェクトを取得します:

// Date -> ZonedDateTime
def zdt2 = date1.toInstant().atZone(ZoneId.systemDefault())
// もしくは
// def zdt2 = ZonedDateTime.ofInstant(date1.toInstant(), ZoneId.systemDefault())
assert( zdt2.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

さて、ちょっと別のタイムゾーンに変換してみましょうか。

def date = Date.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 00:00:00')

// Date -> ZonedDateTime で別の ZoneId に変換
def zdt3 = date.toInstant().atZone(ZoneId.of('Greenwich'))
assert( zdt3.toString() == '2014-03-31T15:00Z[Greenwich]' )

時差のせいで日付と時間がズレてますね。

GDK!
GDK はこれらの変換を簡単にするメソッドを追加しています:

def zdt1 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())

// ZonedDateTime -> Date
def date = zdt1.toDate()
assert( date.toString() == 'Tue Apr 01 00:00:00 JST 2014' )

// Date -> ZonedDateTime その1
def zdt2 = date.toZonedDateTime()
assert( zdt2.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

// Date -> ZonedDateTime その2
def zdt3 = date.toZonedDateTime(ZoneId.of('Greenwich'))
assert( zdt2.toString() == '2014-03-31T15:00Z[Greenwich]')

ZonedDateTime#toDate() は引数なしのものだけ。 Date#toZonedDateTime() は ZoneId を指定して変換可能。 引数を省略した場合は、システムデフォルトのタイムゾーンが使われるもよう。 タイムゾーンを指定して Date オブジェクトに変換する方法はサポートされてないようですね。 そう言えば、旧式の日付 APIjava.util.TimeZone クラスというのがありますが、ZoneId と TimeZone との変換が面倒なためか GDK が追加するメソッドでは Date オブジェクトはデフォルトのタイムゾーンになるものしか使えなさそう。 GDK がどうこう以前に Java API のレベルでタイムゾーンを変換する方法があんまりよくわからない(できるかどうか不明)なので、これは GDK のせいではないような気がします。

Groovy による演算子オーバーロード

GDK は Date クラスに対して

  • previous()/next() ・・・ ++/-- 演算子
  • plus()/minus() ・・・ +/- 演算子
  • upto()/downto() ・・・ for 文のような列挙

のようなメソッドを追加し、演算や制御構造を簡単に書けるようになってまいした:

def date1 = Date.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 00:00:00')

// previous()/next()
date1++
assert( date1.toString() == 'Wed Apr 02 00:00:00 JST 2014' )

// plus()/minus()
def date2 = date1 + 7
assert( date3.toString() == 'Wed Apr 09 00:00:00 JST 2014' )

// upto(), downto()
date1.upto(date3){ d ->
    println d
}

LocalDateTime クラスについてもほぼ同じメソッドが追加されています:

def ldt1 = LocalDateTime.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 01:23:45')

// previous()/next()
ldt1++
assert( ldt1.toString() == '2014-04-01T01:23:45' )

// plus()/minus()
def ldt2 = ldt1 + 7
assert( ldt3.toString() == '2014-04-08T01:23:45' )

// upto(), downto()
ldt1.upto(ldt3){ d ->
    println d
}

Date の場合と同じく、日時に整数を足したり引いたりする場合は日 (day) の値として計算されます。 インクリメント/ディクリメント(++/--)の場合も同じ。

ところで、LocalDateTime (や ZonedDateTime)には GDK に関係なく plus()/minus() メソッドが定義されています。 たとえば plus() メソッドの場合

  • plus(long, TemporalUnit)
  • plus(TemporalAmount)

という2つのオーバーロードがありますが、このうち、2番目の引数が1つの方は GDK によらずに「+」演算子で書くことが出来ます。 ただし、TemporalAmount オブジェクトはいまいちどこか生成・取得すればいいのかよく分かりません。 また、日 (day) 以外にも年や分を加算するメソッド plusYears() や plusMinutes() などのメソッドもあるんですが、これらは残念ながら「+」演算子で使うことはできません。 さぁ、そんな時こそ GDK!

GDK!
そんな時こそ GDK! とか言いつつ、ちょっと GDK の機能というわけでもないのですが、Groovy のチカラを使ってこういった演算を式っぽく書くことがでいます。 これには TemporalCategory というカテゴリクラスを使って、以下のように DSL っぽく書きます:

def ldt1 = LocalDateTime.parse('yyyy-MM-dd hh:mm:ss', '2014-04-01 01:23:45')

use(TemporalCateogry){
    def ldt2 = lat1 + 1.year + 7.day - 1.second
    assert( ldt2.toString() == '2015-04-08T01:23:44' )
}

TemporalCategory カテゴリが1や7のような int 値に getYear() などのメソッド(year プロパティのように書ける)を追加して、TemporalAmout オブジェクトを生成するようにしています。 いちいちカテゴリを使わないといけないのは、こういった int 値に対するメソッドをグローバルに付加するわけにはいかないからでしょう。 ちなみに、このカテゴリは upto()/downto() の拡張版で、ステップ間隔を指定できる step() メソッドにも使えます:

use(TemporalCategory){
    ldt1.step(ldt2, 12.hour){ d ->
        println d
    }
}

// c.f.
ldt1.step(ldt2, 2){ d -> // 間隔が整数なら日(day)指定
    println d
}

残念ながら previous()/next() には日(day)以外を指定できないので、秒のインクリメントなどはできません。

あと、ちょっと細かい部分ですが、Date 同士の引き算は日数を表す int が返されますが、LocalDateTime 同士の引き算は時間(間隔)を与える java.time.Duration オブジェクトになるようです。

その他

後に残ってるのは copyWith() メソッド。 これは LocalDateTime クラスに定義されている withXxxx() メソッドの拡張版です。 LocalDateTime クラスは Date と違ってイミュータブルなので、月や分だけを変更するといったことは出来ませんが、それらだけを変更した別のオブジェクトを生成する withXxxx() メソッドというものが定義されています。 Groovy には @Immutable アノテーションによってイミュータブルにしたクラスには同様のことを行う copyWith() メソッドが生成されるのですが、GDK はこれを模して LocalDataTime に copyWith() メソッドを追加しています。 引数が Map になっていて、使い方は withXxxx() メソッドより楽だと思います(Groovy 上では)

def ldt1 = LocalDateTime.parse('yyyy-MM-dd hh:mm:ss', '2014-04-01 01:23:45')

def ldt2 = ldt1.copyWith(year:2015, second:56)
assert( ldt2.toString() == '2015-04-01T01:23:56' )

手軽ですね。

まとめ

大雑把に GDK が LocalDateTime, ZonedDateTime に追加しているメソッドを見てきましたが、大体のものは Date に追加されたものの焼き直しです。 また、拙者があまり新しい日時 API に精通してるわけでもないため(昔ながらの日付 API にも詳しくないですが)、日時の取り扱いで気をつけるべき部分などをおろそかにしてそうですが(特にタイムゾーンが絡んでるあたり)、Java API の下地になっている概念をきちんと理解した上で、それらを簡単・簡潔に扱うために GDK を活用していくのがいいんじゃないでしょうか。 

Happy April 1st !

日経ソフトウエア 2014年 03月号

日経ソフトウエア 2014年 03月号

プログラミングGROOVY

プログラミングGROOVY

*1:'Asia/Tokyo' 以外に 'Japan' というのがあるのが謎なんですけど・・・

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 コードから使うことがあるのかどうか分かりませんが、ユーティリティ・クラスとして使えなくもないので、念には念を。

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

最近 GDK が List に追加しているメソッドをあれこれ試しているんですが、Java の配列のように要素を取得できる [] 演算子、すなわち getAt() メソッドにいろいろなインデックスを渡したときの挙動がいまいちシックリこないので、もう少しあれこれ試してみました。 putAt() も同じような挙動だと思いますが今回は getAt() のみ。

int 値のインデックス

まずはインデックスとして int 値を渡した場合。これは解くに問題ないですね。 たとえばサイズ4のリストに対して int 値のインデックスを渡した場合を考えてみましょう:

int n = 4
def list = 0..<n    // [0, 1, 2, 3]

(-n-1..n).each{ i ->    // -4 ≦ i ≦ 3 で要素が返される(-5は例外、4はnull)
    print(list[i])
}

これを実行すると、各 i の値に対して以下の結果が得られます:

i list[i]
-5 -
-4 0
-3 1
-2 2
-1 3
0 0
1 1
2 2
3 3
4 null
  • -4 ≦ i ≦ -1(サイズ n のリストの場合は -n ≦ i ≦ -1)の負のインデックスを渡した場合は、末尾から数えて -i 番(-1 なら末尾から数えて -(-1) = 1 番目)の要素が返される(つまりは i + n 番目の要素が返される)
  • -5(サイズ n の場合は -n-1 )以下のインデックスを渡した場合は例外 (ArrayIndexOutOfBoundsException) が投げられる
  • 4(サイズ n の場合は n)以上のインデックスを渡した場合は null を返す

となってます。

末尾を含む範囲のインデックス i..j

次は末尾を含む範囲 i..j をインデックスとして渡した場合の返り値を見ていきます。 例外が投げられずにリストを返す i, j の値は、サイズ n のリストに対して -n ≦ i, j ≦ n-1 の場合。 その範囲外では例外が投げられます。

int n = 4
def list = 0..<n    // [0, 1, 2, 3]

(-n-1..n).each{ i ->    // -4 ≦ i ≦ 3 で返り値を返しうる(端の-5, 4 で例外)
    (-n-1..n).each{ j ->    // -4 ≦ j ≦ 3 で返り値を返しうる(端の-5, 4 で例外)
        print(list[i..j])
    }
    println()
}

このコードを単に実行しても例外が投げられて終わりですが、適当に処理を追加して結果を表にすると以下のようになります:

i\j -5 -4 -3 -2 -1 0 1 2 3 4
-5 - - - - - - - - - -
-4 - [0] [0,1] [0,1,2] [0,1,2,3] [0] [0,1] [0,1,2] [0,1,2,3] -
-3 - [1,0] [1] [1,2] [1,2,3] [1,0] [1] [1,2] [1,2,3] -
-2 - [2,1,0] [2,1] [2] [2,3] [2,1,0] [2,1] [2] [2,3] -
-1 - [3,2,1,0] [3,2,1] [3,2] [3] [3,2,1,0] [3,2,1] [3,2] [3] -
0 - [0] [0,1] [0,1,2] [0,1,2,3] [0] [0,1] [0,1,2] [0,1,2,3] -
1 - [1,0] [1] [1,2] [1,2,3] [1,0] [1] [1,2] [1,2,3] -
2 - [2,1,0] [2,1] [2] [2,3] [2,1,0] [2,1] [2] [2,3] -
3 - [3,2,1,0] [3,2,1] [3,2] [3] [3,2,1,0] [3,2,1] [3,2] [3] -
4 - - - - - - - - - -
  • 「-」の部分は例外が投げられる
  • 赤い部分は逆順序のリストが返される部分
  • i, j ともに -4~-1 の部分と 0~3の部分が同じになっています(サイズnの場合は -n~-1 の部分と 0~n-1 の部分)
  • インデックスとして負の整数を使わない場合に対応するのは表の右下4分の1の箇所(かつ赤くない部分)

範囲を指定する際に両方を負の整数にするなんてあんまり意味なさそうですが、まぁそういう場合もリストを返す場合がありますよ、と。 範囲の端(from と to)が負なら、それぞれリストのサイズ n を加えたものを新たな範囲の端として使用して、対応する結果を返す感じですね。

末尾を含まない範囲のインデックス i..<j

末尾を含まない範囲 i..<j の場合は j の値として1つ小さい値を設定した場合の、末尾を含むインデックスになりそうですが、というか大体はなるのですが、少々違う箇所があります。 まず i = j のときは i..<i は空範囲となり、返り値は空リストになります。 これは末尾を含む範囲にはあり得なかったものです。 また、j = i ± 1 のときは i だけからなる範囲になります。 これは末尾を含む範囲の場合の i = j と同じになります。 これら2つの場合がちょっと注意が必要かと。

int n = 4
def list = 0..<n    // [0, 1, 2, 3]

(-n-1..n).each{ i ->    // -4 ≦ i ≦ 3 で返り値を返しうる(端の-5, 4 で例外)
    (-n-2..(n+1)).each{ j ->    // -5 ≦ j ≦ 4 で返り値を返しうる(端の-6, 5 で例外)
        println(list[i..<j])
    }
    println()
}

範囲を含む場合と同様に結果を表にすると以下のようになります:

i\j -6 -5 -4 -3 -2 -1 0 1 2 3 4 5
-5 - [] - - - - - - - - - -
-4 - [0] [] [0] [0,1] [0,1,2] [0,1,2,3] [0] [0,1] [0,1,2] [0,1,2,3] -
-3 - [1,0] [1] [] [1] [1,2] [1,2,3] [1,0] [1] [1,2] [1,2,3] -
-2 - [2,1,0] [2,1] [2] [] [2] [2,3] [2,1,0] [2,1] [2] [2,3] -
-1 - [3,2,1,0] [3,2,1] [3,2] [3] [] [3] [3,2,1,0] [3,2,1] [3,2] [3] -
0 - [0] [0,1] [0,1,2] [0,1,2,3] [0] [] [0] [0,1] [0,1,2] [0,1,2,3] -
1 - [1,0] [1] [1,2] [1,2,3] [1,0] [1] [] [1] [1,2] [1,2,3] -
2 - [2,1,0] [2,1] [2] [2,3] [2,1,0] [2,1] [2] [] [2] [2,3] -
3 - [3,2,1,0] [3,2,1] [3,2] [3] [3,2,1,0] [3,2,1] [3,2] [3] [] [3] -
4 - - - - - - - - - - [] -
  • 「i = j」の左上から右下に走る対角線の部分が空範囲の指定に対応し、空リスト [] を返しています
  • 「j = i ± 1」 となる、上記の「i = j」 の上下の斜め線の部分が要素が i 1つからなる範囲に対応し、返り値のリストは1つの要素のみを持ちます
  • それ以外は末尾を含む範囲の場合で j の値を1つずらした場合に対応。 対角線に対して右上と左下の三角形でずれ方が逆ですけどね。

んー、なんか表にしてみると前より理解できたような、余計に分からなくなったような・・・

プログラミングGROOVY

プログラミングGROOVY

GDK のList がこんなに可愛いわけがない。 (List 編3) シーケンス

f:id:waman:20130628013258p:plain

今回は GDK が List に追加しているメソッドのうち、関数型言語にある型、シーケンスに関するメソッドを見ていきます。 シーケンスは順序づけられたオブジェクトの列といったイメージで、要素列の走査は1度しか行われない気持ちです*1。 Groovy にはシーケンスという型はありませんが、順序を持つ集合である List インターフェースがその役割を担っていると言っていいでしょう。 まぁ、java.lang.Iterable や java.util.Iterator も今回扱うメソッドと同じものがいくつか定義されているので、それらもシーケンスのように扱うことができますが。

今回見ていくメソッドは以下のもの:

Object head()
List tail()

Object first()
Object last()

List take(int num)
List drop(int num)

List takeWhile(Closure condition)
List dropWhile(Closure condition)

List collate(int size)
List collate(int size, int step)
List collate(int size, boolean keepRemainder)
List collate(int size, int step, boolean keepRemainder)

List reverse()
List reverse(boolean mutate)
List reverseEach(Closure closure)

head(), tail() や take(), drop() などはそのままの名前で関数型言語に定義されていることが多いかと。 同じ名前で挙動がことなる、なんてややこしいことにはなってないはず・・・ 各メソッドをもう少し詳しく見るとこんな感じ

メソッド 返り値 since 説明
head()
tail()
first()
last()
Object
List
Object
Object
1.5.5
1.5.6
1.5.5
1.5.5
シーケンスの先頭、残り、最後の要素などを取得する
take(int)
drop(int)
takeWhile(Closure)
dropWhile(Closure)
List 1.8.1
1.8.1
1.8.7
1.8.7
引数で指定された数の要素、もしくは条件にあう要素を取捨する
collate(int)
collate(int, int)
collate(int, boolean)
collate(int, int, boolean)
List 1.8.6 引数で指定された数とステップ数で要素をグループ化(List オブジェクト)して、そのグループの List を返す
reverse()
reverse(boolean)
reverseEach(Closure)
List 1.0
1.8.1
1.5.0
シーケンスを逆順で返す

ではそれぞれのサンプル・コードを。

head(), tail(), fiirst(), last() メソッド

head(), tail() は先頭とそれを除いた残り。 あわせて元の List になります。 tail() は List オブジェクト(を返す)ことに注意。 first() と last() はまぁそのまま最初と最期の要素。 head() と first() は同じだよね。 使っているコンテキストで head-tail / first-last を使い分けるとコードが読みやすい、って程度のことかと。

def list = 0..9    // 範囲もリスト

// head(), tail()
assert list.head() == 0
assert list.tail() == (1..9)    // List オブジェクトを返すヨ

// first(), last()
assert list.first() == 0
assert list.last() == 9

まぁ、問題なし。

take(), drop(), takeWhile(), dropWhile() メソッド

take(), drop() は指定した数だけ(先頭から)要素を取得、または削除した List を返します。 一方、takeWhile() と dropWhile() は引数で指定した boolean 値を返すクロージャが true を返している間、(先頭から)要素を取得、または削除した List を返します。 同じ引数を指定した場合、take() と drop() または takeWhile() と dropWhile() が元のリストに対して相補的に要素を分けます(head() と tail() みたいに)。

def list = 0..9

// take(), drop()
assert list.take(4) == (0..<4)
assert list.drop(4) == (4..9)

// takeWhile(), dropWhile()
assert list.takeWhile{ it < 4 } == (0..<4)
assert list.dropWhile{ it < 4 } == (4..9)

collate() メソッド

collate」は照合する、順に揃えるという意味の英単語らしいです。 元のリストの要素を、指定した個数ずつグループ(List オブジェクト)にして、それを別に指定したステップ数だけ飛ばして作成していったものを List として返します・・・ うーむ、説明が自分でも分からん、ということでコードを見てちょ:

//***** collate(int) *****
assert list.collate(3) == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    // 3個(第1引数)ずつのグループ。 グループの先頭は3個飛ばし

//***** collate(int, int) *****
assert list.collate(3, 2) == [[0, 1, 2], [2, 3, 4], [4, 5, 6], [6, 7, 8], [8, 9]]
    // 3個(第1引数)ずつのグループ。 先頭は2個(第2引数)飛ばし

assert list.collate(3, 2).collect{ it.first() } == [0, 2, 4, 6, 8]
    // 各要素のリストは、先頭だけ見ると第2引数の個数だけ飛ばしてる

assert list.collate(3) == list.collate(3, 3)
    // 引数1つは、第1、第2引数に同じ数値を指定したのと同じ

//***** collate(int, bookean) *****
assert list.collate(3, true) == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    // 最後に残った要素を1つのリストにする(true)

assert list.collate(3, false) == [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
    // 最後に残った要素は捨てる(false)

assert list.collate(3) == list.collate(3, true)
    // デフォルトでは true を指定したのと同じ

//***** collate(int, int, boolean) *****
assert list.collate(3, 2, false) == [[0, 1, 2], [2, 3, 4], [4, 5, 6], [6, 7, 8]]
    // 2つ目と3つ目を合わせたもの

assert list.collate(3, 2, false).collect{ it.first() } == [0, 2, 4, 6]
assert list.collate(3, 2, false).every{ it.size() == 3 }

自分で同じものを実装しようとは思わないので、使うときが来たら便利そう。

reverse(), reverseEach() メソッド

最後は逆順序で要素を返す reverse(), reverseEach() メソッド。 reverse() は List オブジェクトとして逆順序のリストを返すのに対して、reverseEach() は each() メソッド同様にクロージャを引数にとってその処理を要素に適用しますが、その適用順が逆だってだけですね。 reverse() メソッドには元の要素を変更するかどうかを指定する boolean 値を渡せますが、true を渡すと UnsupportedOperationException が投げられよるw(Groovy 2.1.4)

// reverse()
def list1 = (0..9)
def list2 = list1.reverse()    // mutate=false
assert list2 == (9..0)
assert list1 == (0..9)

def list3 = (0..9)
try{
    def list4 = list3.reverse(true)
    assert false
}catch(UnsupportedOperationException e){ assert true }    // サポートされてないんかい!

def list5 = (0..9)
list5.reverseEach{ print it }    // 9876543210
assert list5 == (0..9)    // mutate=false

まぁ、特に問題はないかと。

次回で一応、GDK が List に追加しているメソッドは網羅の予定。 残りのメソッドをやっつけるヨ!(予定)

プログラミングGROOVY

プログラミングGROOVY

*1:今の場合、扱っているのは List なので何度でも走査することができるんですが。 そう言う意味では Iterator とかの方がシーケンスのイメージにあってますね。

GDK が List に追加している getAt() と putAt() に指定できるインデックス範囲についての絞殺

『GDK の List がこんなに可愛いわけがない。』の List 編1List 編2で、GDK が List インターフェースに追加しているメソッド getAt(), putAt() を見てきました。 そのときに API を見たりサンプルコードを書いたりしていて思ったのですが、インデックスを指定して要素を取得もしくは設定する getAt() と putAt() は、指定できるインデックスについて対応関係がある(というか同じインデックスの型を指定できる)のが自然だと思うんですが、一部(半分)がちょっと指定できる型にズレがあって妙なことになっております。 その辺りについて考察というか、同じインデックスを指定した場合の挙動のズレみたいなの見ていきたいと思います。 普通に使う分には特に問題ないので重箱の隅を突つく感が否めませんが。

f:id:waman:20130628013258p:plain

GDK が List インターフェースに追加している getAt(), putAt() メソッドには以下のようなシグニチャがあります:

// int
Object getAt(int idx)
void putAt(int idx, Object value)

// EmptyRange
List getAt(EmptyRange range)
void putAt(EmptyRange range, Object value)
void putAt(EmptyRange range, Collection value)

// Range / IntRange
List getAt(Range range)
void putAt(IntRange range, Object value)
void putAt(IntRange range, Collection col)

// Collection / List
List getAt(Collection indices)
void putAt(List splice, Object value)
void putAt(List splice, List values)

以下では putAt() の第2引数としては Object のものを扱います。

int 値のインデックス

int 値でインデックスを指定するものは問題ないでしょう。 負のインデックスは末尾から数えるとかありましたけど。

def list0 = 'abcd'.collect()
assert list0[1] == 'b'

list0[2] = 'x'
assert list0 == 'abxd'.collect()

// サイズ以上のインデックス
def list1 = 'abcd'.collect()
assert list1[6] == null

list1[6] = 'x'
assert list1 == ['a', 'b', 'c', 'd', null, null, 'x']

// 負のインデックス
def list2 = 'abcd'.collect()
assert list2[-1] == 'd'

list2[-3] = 'x'
assert list2 == 'axcd'.collect()

EmptyRange のインデックス

次の EmptyRange でインデックスを指定するものは、getAt() は空リストを返し、putAt() はその位置への挿入をするのでした。

// getAt(int)
def list0 = 'abcd'.collect()
assert list0[0..<0] == []
assert list0[1..<1] == []

// putAt(int, Object)
list0[0..<0] = 'x'
assert list0 == 'xabcd'.collect()

list0[2..<2] = 'y'
assert list0 == 'xaybcd'.collect()

同じ EmptyRange でも 0..<0, 2..<2 を指定した場合に putAt() の挙動が異なるのが違和感あるような・・・

Range / IntRange のインデックス

さて、この辺りからが getAt() と putAt() でインデックスの型が違ってるメソッド。 ちなみに Range はインターフェースで、IntRange は Range を実装したクラスです。 Range を実装したクラスは他に(groovy.lang パッケージ内で)EmptyRange, ObjectRange というのがあります:

package groovy.lang

interface Range extends java.util.List
class EmptyRange  extends java.util.AbstractList implements Range
class IntRange    extends java.util.AbstractList implements Range
class ObjectRange extends java.util.AbstractList implements Range

これらはコンストラクタで境界 (from, to)*1 の値を指定してインスタンス化します。 また、これらのクラスは不変、すなわち要素を変更することはできません*2。 これを踏まえて、まずは通常の使い方。 次のサンプルでは IntRange オブジェクト (0..2) でインデックスを指定しています。 IntRange は Range のサブクラスなので、これはまぁ意図された普通の使い方でしょう:

assert (0..2) instanceof IntRange

def list0 = 'abcd'.collect()

// getAt(Range)
assert list0[0..2] == 'abc'.collect()

// putAt(IntRange, Object)
list0[0..2] = 'x'
assert list0 == 'xd'.collect()

さて。 getAt() の引数は Range で putAt() の引数が IntRange なので、Integer を要素に持つ ObjectRange でインデックスを指定してやるとどうなるでしょう? 実際にやってみましょう:

def index = new ObjectRange(0, 2)
assert index instanceof Range
assert !(index instanceof IntRange)    // IntRange オブジェクトではない

// getAt(Range) が呼び出されている
def list0 = 'abcd'.collect()
assert list0[index] == 'abc'.collect()

// putAt(List, Object) が呼び出されている!
list0[index] = 'x'
assert list0 == 'xxxd'.collect()    // さっきは 'xd'.collect() になってた

getAt() の方はインデックスの型が Range なので IntRange でも ObjectRange でも挙動は同じでしょうね。 一方、putAt() はインデックスは IntRange 型を要求しているので ObjectRange を渡すとこのメソッドは呼ばれません。 代わりに(Range が List のサブタイプなので) putAt(List, Object) が呼ばれます。 で、putAt(IntRange, Object) はその範囲のサブリストを1つの(第2引数で指定された)オブジェクトに置き換え、putAt(List, Object) はリストで指定された位置の要素をそれぞれ(第2引数で指定された)オブジェクトに置き換えるので、結果が異なります。 今の場合は getAt() の方が正しそう。 putAt() の引数も Range にした方がよいかと*3

Collection / List のインデックス

この型でインデックスを指定する場合、通常は List として指定する用途を想定してるんでしょう:

def list0 = 'abcd'.collect()

// getAt(Collection)
assert list0[1, 3] == 'bd'.collect()

// putAt(List, Object)
list0[1, 3] = 'x'
assert list0 == 'axcx'.collect()

これはまぁ、そのままって感じですかね。 putAt() は前の節で出てきたヤツですが、これだけ書いてみると違和感はないかと。 さて、getAt() ではインデックスの型が List ではなく Collection になっているので、例えば Set でインデックスを指定してみましょう:

def list0 = 'abcd'.collect()

def index = [1, 3] as HashSet
assert list0[index].toSet() == 'bd'.collect().toSet()

これはそのままの挙動といえばそのままなんですが、インデックスを Set で指定したら返り値も List ではなく Set で返して欲しい気がするところ。 ただ、これは Groovy 的には実装が難しいんでしょうね。 そういうメソッドはジェネリクスを使って定義しないといけないけど、Groovy は基本的にジェネリクス無視だし、何より Scala などと違って、List や Set などのインターフェースにデフォルトの実装が伴っていないので、引数の型だけからそれにあったコレクションのインスタンスを作るのが難しい(ユーザー定義のコレクションとか出てきたら基本無理)のが原因でしょう。 今の場合は getAt() のインデックスの型が List でいいんじゃないかと。 ちなみに当然のことながら、putAt() に Set のインスタンスをインデックスとして渡すと MissingMethodException が投げられます。

今回は List に定義されている GDK のメソッド getAt(), putAt() でインデックスの型にズレがあるのを見てきました。 インデックスを範囲のリテラル(1..3 のようなもの)で指定してる分(文)には特に問題がありませんが、シグニチャを見てると違和感が拭えないところ。 このズレが意図的なのか特に気にしてないのかよく分かりませんが、ちょっとコード書いた人を絞めたくなる気分w とは言え、古くからあるメソッドなのでそんなに問題は起きなてないんでしょう。 まぁ、自分が使う分には変な使い方をしないようにしておくのが無難なんでしょうけどね。

プログラミングGROOVY

プログラミングGROOVY


クビシメロマンチスト 人間失格・零崎人識 (講談社文庫)

クビシメロマンチスト 人間失格・零崎人識 (講談社文庫)

*1:EmptyRange の場合は1つの値

*2:UnsupportedOperationException が投げられます。

*3:ObjectRange をコンストラクタからインスタンス化できないようにするという手もあるけど。

GDK のList がこんなに可愛いわけがない。 (List 編2) 要素の追加・削除

f:id:waman:20130628013258p:plain

今回は GDK が List インターフェースに追加しているメソッドのうち、要素の追加・削除に関連するメソッド。 前回見た要素の取得に関するメソッド getAt() と対になっている putAt() メソッドなどを見ていきます。 今回見ていくメソッド:

boolean addAll(int index, Object[] items)

void putAt(int idx, Object value)
void putAt(EmptyRange range, Object value)
void putAt(EmptyRange range, Collection value)
void putAt(IntRange range, Object value)
void putAt(IntRange range, Collection col)
void putAt(List splice, Object value)
void putAt(List splice, List values)

List plus(int index, Object[] items)
List plus(int index, List additions)
List plus(int index, Iterable additions)

List minus(Object removeMe)
List minus(Collection removeMe)
List minus(Iterable removeMe)
  • putAt() のインデックスを指定する範囲やコレクションが getAt() のものと違うのが悩ましい・・・ なんでこんなことになってんるんだ?
  • plus() メソッドは、引数が1つなら + 演算子として使えますが、ここで定義されているものは引数を2つ(以上)とるのでメソッド呼び出しとしてしか使えません

要素の追加に関しては挿入なのか置き換え(書き換え)なのかを注意する必要がありますね。 それと元のリストを変更するのか追加・削除したリストを返すのか(不変・可変)も重要ですね。 後でサンプルを見ていきます。

メソッド 返り値 since 説明
addAll(int, Object[]) boolean 1.7.2 指定された位置に要素を挿入する。 可変長引数として使える
putAt(int, Object)
putAt(EmptyRange, Object)
putAt(EmptyRange, Collection)
putAt(IntRange, Object)
putAt(IntRange, Collection)
putAt(List, Object)
putAt(List, List)
void 1.0
1.0
1.0
1.0
1.5.0
1.0
1.0
指定された位置の要素を置き換える。 空範囲の場合は挿入する
plus(int, Object[])
plus(int, List)
plus(index, Iterable)
List 1.8.1
1.8.1
1.8.7
元のリストは変更せずに、要素を挿入したリストを新たに作成して返す
minus(Object)
minus(Collection)
minus(Iterable)
List 1.0
1.0
1.8.7
元のリストは変更せずに、要素を削除したリストを新たに作成して返す

plus() や minus() はコレクションで演算子として使った場合に、元が不変で変更されたコレクションを返してたのと同様に、リストの場合も変更されされたリストを返します。 ただし、今の場合は plus() を演算子として使えませんが。

addAll() メソッド

addAll() メソッドはインデックスで指定した位置に引数の配列の要素を挿入します。 Groovy では引数最後の配列は可変長引数として扱われるので、addAll() の最後には複数の要素を書くことが出来ます:

def langs = ['Java', 'Java']
langs.addAll(1, 'Groovy', 'Scala')    // 可変長
assert langs == ['Java', 'Groovy', 'Scala', 'Java']

Java 標準 API の addAll(int, Collection) と挙動は同じですね。 使いやすさは向上しております。

putAt() メソッド

putAt() は結構たくさんシグニチャがありますが、

  • インデックスが
    • int
    • EmptyRange
    • IntRange
    • List
  • 置き換えるものが
    • Object
    • Collection, List

という風に分類できます(定義されていない組合せもありますが)。 ちょっと getAt() の場合とインデックスの型が異なりますが、これは別記事で比べたいと思います。 今回は仕様通りに。 putAt() は全て元のリストを変更します。 まずは int 値のみで位置を指定する場合:

// putAt(int, Object)
def langs0 = ['Java', 'Java']
langs0[1] = 'Groovy'
assert langs0 == ['Java', 'Groovy']

add() や addAll() と異なり値を置き換えます。 まぁ、標準 API の set() ですね。 この場合は置き換えるものとしてコレクションは指定できません。 次は EmptyRange(空の範囲)を指定した場合:

// putAt(EmptyRange, Object)
def langs1 = ['Java', 'Java']
langs1[0..<0] = 'Groovy'
assert langs1 == ['Groovy', 'Java', 'Java']

def langs2 = ['Java', 'Java']
langs2[2..<2] = 'Groovy'
assert langs2 == ['Java', 'Java', 'Groovy']

// putAt(EmptyRange, Collection)
def langs3 = ['Java', 'Java']
langs3[0..<0] = ['Groovy', 'Scala']
assert langs3 == ['Groovy', 'Scala', 'Java', 'Java']

この場合は範囲のスタート位置にオブジェクト(もしくはコレクション)が挿入されます。 0..<0 と 2..<2 は同じから要素でも挙動が(挿入される位置が)異なります。 こんな挙動でいいんだ・・・ なんかあんまり使わない方が良さそうな感じのメソッドだにゃあ。 次は IntRange が指定された場合:

// putAt(IntRange, Object)
def langs4 = ['Java', 'Java', 'Java']
langs4[1..2] = 'Groovy'
assert langs4 == ['Java', 'Groovy']

// putAt(IntRange, Collection)
def langs5 = ['Java', 'Java', 'Java']
langs5[1..2] = ['Groovy', 'Scala', 'Clojure']
assert langs5 == ['Java', 'Groovy', 'Scala', 'Clojure']

指定された範囲の要素はすべて削除され、代わりに指定したオブジェクトもしくはコレクションで置き換えられます。 指定した範囲とコレクションのサイズが違っても大丈夫です(リストのサイズも変わりますが)。 最後は引数に(Integer の)リストを指定した場合:

// putAt(List, Object)
def langs6 = ['Java', 'Java', 'Java', 'Java']
langs6[1, 3] = 'Groovy'
assert langs6 == ['Java', 'Groovy', 'Java', 'Groovy']

// putAt(List, List)
def langs7 = ['Java', 'Java', 'Java', 'Java']
langs7[1, 3] = ['Groovy', 'Scala']
assert langs7 == ['Java', 'Groovy', 'Java', 'Scala']

def langs8 = ['Java', 'Java', 'Java', 'Java']
try{
    langs8[1, 3] = ['Groovy', 'Scala', 'Clojue']
        // 指定したインデックスの個数とリストの個数は一致しているベシ
    assert false
}catch(IllegalArgumentException e){
    assert true
}

第2引数がオブジェクトの場合は指定したインデックスの位置を全てそのオブジェクトで置き換えます。 一方、第2引数がリストの場合はそれぞれ対応したインデックスの位置を指定したリストの要素で置き換えるので、このインデックス(のリスト)のサイズと指定したリストのサイズは一致している必要があります。 もし異なっていれば IllegalArgumentException が投げられます。

plus() メソッド

plus() メソッドは要素を挿入したリストを新たに生成して返します。 元のリストは変更されません。 通常、Groovy では plus() メソッドは+演算子を使用するために定義しますが、今の場合引数が2つ(以上)あるので+演算子としては使えません。 まぁ、使い方は簡単:

// plus(int, Object[])
def langs9 = ['Java', 'Java']
def result9 = langs9.plus(1, 'Groovy', 'Scala')    // 可変長
assert langs9 == ['Java', 'Java']
assert result9 == ['Java', 'Groovy', 'Scala', 'Java']

// plus(int, List)
def langs10 = ['Java', 'Java']
def result10 = langs10.plus(1, ['Groovy', 'Scala'])
assert langs10 == ['Java', 'Java']
assert result10 == ['Java', 'Groovy', 'Scala', 'Java']

minus() メソッド・- 演算子

minus() メソッドは remove(), removeAll() と同じように指定した要素を削除しますが、元のリストは変更せずに、要素を削除したリストを返します。 minus() は上記の plus() と違って-演算子で書けます。

// minus(Object)
def langs11 = ['Java', 'Groovy', 'Scala', 'Clojure']
def result11 = langs11 - 'Java'    // minus() メソッド
assert langs11 == ['Java', 'Groovy', 'Scala', 'Clojure']
assert result11 == ['Groovy', 'Scala', 'Clojure']

// minus(Collection)
def langs12 = ['Java', 'Groovy', 'Scala', 'Clojure']
def result12 = langs12 - ['Groovy', 'Scala']
assert langs12 == ['Java', 'Groovy', 'Scala', 'Clojure']
assert result12 == ['Java', 'Clojure']

def langs13 = ['Java', 'Groovy', 'Scala', 'Clojure']
try{
    def result13 = langs13.minus('Groovy', 'Scala')
        minsu(Object[]) はないので可変長引数としては書けない
    assert false
}catch(MissingMethodException e){
    assert true
}

minus(Object) というシグニチャは定義されていないので、addAll(Object) のように可変長引数のようには書けません。

次回はシーケンスというか、順序に関連したメソッドを見ていく予定。 ただ、getAt(), putAt() のインデックスに関する番外編を書くかも。

プログラミングGROOVY

プログラミングGROOVY

GDK のList がこんなに可愛いわけがない。 (List 編1) 要素の取得

f:id:waman:20130628013258p:plain

前回までで GDK が Object クラスや Collection クラスに追加しているメソッドを見てきました。 ただ、普段よく使う型はこれらというより今回から見ていく List でしょう。 Object や Collection に追加されていたメソッドのサンプルを書いているときも、基本 List オブジェクトを使ってました。 『プログラミングGROOVY』にチラッと書いてましたが、コレクションとしてのメソッドが Object クラスに定義されているのは通常のオブジェクトとコレクションを区別せずに使えると便利なためだそうで、まぁ言ってみればコレクションとしてのメソッドを便利に扱いたいってことの裏返しですね*1。 ということで、List インターフェースに追加されているメソッドは、基本的にインデックスを伴った操作や順序に関連する操作となっています。

ちなみにトップの画像は『俺妹ジェネレータ』より。 コレクションのときにやろうと思ったんだけど、「コレクション」や「Collection」が長すぎて拒否られたもんで・・・

目次
List 編の予定内容は次の通り:

順調にいけばだけど。 今回は要素の取得に関するメソッド。

Object getAt(int idx)
List getAt(EmptyRange range)
List getAt(Range range)
List getAt(Collection indices)

List withDefault(Closure init)
List withLazyDefault(Closure init)
List withEagerDefault(Closure init)
メソッド 返り値 since 説明
getAt(int) Object 1.0 引数のインデックスで指定された要素を取得。 []演算子を使える。
getAt(EmptyRange)
getAt(Range)
getAt(Collection)
List 1.0 引数のインデックスのコレクションで指定された要素をリストとして取得。 []演算子を使える。
wtihDefault(Closure)
withLazyDefault(Closure)
withEagerDefault(Closure)
List 1.8.7 指定されたインデックスがサイズを超えていたときの挙動を指定する

getAt() メソッド
getAt() メソッドは引数が int もしくはそのコレクションなら、それをインデックスとして指定された要素を返します。 インデックスがコレクションなら、返り値はリストになります(この辺り、型が適当な気も・・・)。 Groovy では getAt() メソッドは Java の配列で要素を取得する [] 演算子を使えます。 ちょっと慣れないとややこしいのは負のインデックスを指定した場合でしょうかね。 「-」をとって末尾から数えればいいだけですけどね。 これに範囲 (Range) が混ざってくると少し大変。 まぁ、とりあえず簡単なサンプル・コードを:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby']
assert langs.size() == 6

// getAt(int)
assert langs[0] == 'Java'
assert langs[5] == 'JRuby'
assert langs[7] == null    // (サイズ-1)を超えたら null
assert langs[-1] == 'JRuby'    // 負のインデックスは末尾から
assert langs[-2] == 'Jython'

// getAt(EmptyRange)
assert langs[0..<0].isEmpty()    // 0..<0 は空

// getAt(Range)
assert langs[1..3] == ['Groovy', 'Scala', 'Clojure']    // 1, 2, 3番目
assert langs[3..<5] == ['Clojure', 'Jython']    // 3, 4番目。 5番目は入らない
assert langs[0..0] == ['Java']    // 0 だけからなる範囲ね
assert langs[2..-1] == ['Scala', 'Clojure', 'Jython', 'JRuby']    // 2番目から最後まで

// getAt(Collection)
assert langs[1, 5, 2] == ['Groovy', 'JRuby', 'Scala']    // 指定したインデックスの通りに要素を取得

インデックスが0~サイズ-1の範囲を超えた場合でも、例外が投げられません。 上記の例でちょっと分かりにくそうなのは getAt(Range) の最後のもので、インデックスとして「2..-1」を指定しているものでしょうか。 これは2番目から最後の要素までを取得したいといったときに使います。 まぁ、負の境界を持つ範囲はこの定型コード以外にあまり使わない方が身のため(みんなのため?)だと思いますが、一応、挙動としては以下のようになってます:

def ints = (0..5)

assert ints[3..<0]  == [3, 2, 1]
assert ints[3..0]   == [3, 2, 1, 0]
assert ints[3..<-1] == [3, 2, 1, 0]
assert ints[3..-1]  == [3, 4, 5]
assert ints[3..<-2] == [3, 4, 5]
assert ints[3..-2]  == [3, 4]

1~3番目のものは3から逆順に要素を返してますが、4~6番目は3から正順に要素を返してます。 違いは範囲に入っている整数を具体的に考えて、負の数が入っているなら正順に負のインデックスで指定される要素まで、0以上の整数しか含まれていないならそのインデックス群で指定される要素群を返す、といった挙動のようです。 まぁ、あんまり多用しない方がいいでしょう。

withDefault(), withLazyDefault(), withEagerDefault() メソッド
次は、要素取得の際にインデックスが範囲を超えていた場合の挙動を指定できるメソッド。 withDefault() は withLazyDefault() と全く同じ挙動だそうなので、以下では withLazyDefault() と withEagerDefault() を見ていきます。 これらはどちらも指定されたインデックスが範囲を超えていた場合に、そのインデックスの位置にクロージャを実行した結果がセットされてそれがメソッドの返り値として返されるのですが、違いはその要素までの位置の要素を後で初期化するか (lazy)すぐに初期化するか (eager) です。 値の計算にコストがかかる場合は withLazyDefault() を使いましょう。 実際に使ってみるとこんな感じ:

// withLazyDefault(Closure), withDefault(Closure)
def java = ['Java'].withLazyDefault{ i -> "Java$i" }
assert java[8] == 'Java8'
assert java.join(', ') == 'Java, null, null, null, null, null, null, null, Java8'

// withEagerDefault(Closure)
def groovy = ['Groovy'].withEagerDefault{ i -> "Groovy$i" }
assert groovy[2] == 'Groovy2'
assert groovy.join(', ') == 'Groovy, Groovy1, Groovy2'

初期化が行われるのは get(), getAt() を呼び出したときだけのようです。 えー、ちょっと注意が必要なのは withLazyDefault(), withEagerDefault() メソッドが呼び出されたリストは、範囲外の要素を取得しようとしても null を返すけど、返り値のリストが変更されると元のリストも同じように要素が変更されるトコロでしょうか。

def langs0 = ['Java']    // 元リスト
def langs1 = langs0.withLazyDefault{ "Java$it" }    // withLazyDefault() の返り値

// 返り値リストで要素取得
assert langs1[3] == 'Java3'
assert langs1.join(', ') == 'Java, null, null, Java3'
assert langs0.join(', ') == 'Java, null, null, Java3'    // 変わってる!

// 元のリストで要素取得
assert langs0[5] == null    // 取得できない
assert langs0.join(', ') ==  'Java, null, null, Java3'
assert langs1.join(', ') ==  'Java, null, null, Java3'

// 結局のところ
assert !langs0.is(langs1)
assert langs0 == langs1

ようは元リストと返り値リストは一緒に使わないようにしよう(というか返り値リストだけ使おう)ということで。 まぁ、言われなくてもそうするだろうけどw

次回は要素の追加・削除に関するメソッドを見ていく予定。

プログラミングGROOVY

プログラミングGROOVY

  • 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/07/06
  • メディア: 単行本(ソフトカバー)
  • 購入: 6人 クリック: 392回
  • この商品を含むブログ (155件) を見る

*1:なんか、前の記事で Object クラスにコレクション用っぽいメソッドがあれこれ定義されているのはコレクションと配列に共通したメソッドを一箇所に集めたい、みたいなことを書いた気もしますが・・・ まぁ、そういうのもあるんちゃう?