第四回オフラインリアルタイムどう書くの解答例(Java + Spock)

第四回オフラインリアルタイムどう書くの解答例。
問題は
http://nabetani.sakura.ne.jp/hena/ord4tetroid/
続々集まっている回答を眺めていたら、
http://d.hatena.ne.jp/y_sumida/
が目に止まり、Spock いいなぁと思い、テストは spock で。

まずは本体。

```java:Tetroid.java
import java.util.Collection;
import java.util.TreeSet;
import java.util.Arrays;

public class Tetroid
{
  static abstract class Identifier
  {
    abstract char getName();
    abstract boolean match(TreeSet<String> items);
    static String dec( String a, String b )
    {
      StringBuilder r = new StringBuilder();
      for( int i=0 ; i<a.length() ; ++i ){
        r.append( (char)( a.charAt(i) - b.charAt(i) + '3') );
      }
      return r.toString();
    }
    static String[] normalize( Collection<String> v )
    {
      String[] result = new String[v.size()];
      int ix=0;
      for( String s : v ){
        result[ix] = dec( s, v.iterator().next() );
        ++ix;
      }
      return result;
    }
    TreeSet<String> exchange_xy( TreeSet<String> items )
    {
      TreeSet<String> result = new TreeSet<String>();
      for( String s : items ){
        StringBuilder b = new StringBuilder( s );
        result.add( b.reverse().toString() );
      }
      return result;
    }
  }
  static class TetroidI extends Identifier
  {
    char getName(){  return 'I'; }
    boolean match(TreeSet<String> items)
    {
      String first = items.first();
      String last = items.last();
      System.err.flush();
      if ( first.charAt(0)==last.charAt(0) ){
        return (first.charAt(1) + 3) == last.charAt(1);
      } else if ( first.charAt(1)==last.charAt(1) ){
        return (first.charAt(0) + 3) == last.charAt(0);
      }
      return false;
    }
  }
  static class TetroidO extends Identifier
  {
    char getName(){  return 'O'; }
    boolean match(TreeSet<String> items)
    {
      String[] normalized = normalize(items);
      return Arrays.deepEquals(normalized, new String[]{ "33", "34", "43", "44" } );
    }
  }
  static abstract class TetriodLTS extends Identifier
  {
    abstract String[][] getCandidates();
    boolean match(TreeSet<String> items)
    {
      String[] normalized = normalize( items );
      for( String[] candidate : getCandidates() ){
        if ( Arrays.deepEquals( normalized, candidate )){
          return true;
        }
      }
      return false;
    }
  }
  static class TetriodT extends TetriodLTS
  {
    char getName(){  return 'T'; }
    static final String[][] candidates = {
        new String[]{ "33", "43", "44", "53" },
        new String[]{ "33", "42", "43", "53" },
    };
    String[][] getCandidates(){  return candidates; }
    boolean match(TreeSet<String> items)
    {
      return super.match( items ) || super.match( exchange_xy( items) );
    }
  }
  static class TetriodS extends TetriodLTS
  {
    char getName(){  return 'S'; }
    static final String[][] candidates = {
        new String[]{ "33", "34", "44", "45" },
        new String[]{ "33", "42", "43", "52" },
    };
    String[][] getCandidates(){  return candidates; }
    boolean match(TreeSet<String> items)
    {
      return super.match( items ) || super.match( exchange_xy( items) );
    }
  }
  static class TetriodL extends TetriodLTS
  {
    char getName(){  return 'L'; }
    static final String[][] candidates = {
        new String[]{ "33", "34", "35", "45" },
        new String[]{ "33", "43", "52", "53" },
        new String[]{ "33", "43", "44", "45" },
        new String[]{ "33", "34", "43", "53" },
    };
    String[][] getCandidates(){  return candidates; }
    boolean match(TreeSet<String> items)
    {
      return super.match( items ) || super.match( exchange_xy( items) );
    }
  }
  static final Identifier[] identifiers={
      new TetroidI(),
      new TetroidO(),
      new TetriodT(),
      new TetriodS(),
      new TetriodL(),
  };
  char getName( String input )
  {
    TreeSet<String> items = new TreeSet<String>(Arrays.asList(input.split(",")));
    if ( items.size() == 4 ){
      for( Identifier i : identifiers){
        if ( i.match( items)){
          return i.getName();
        }
      }
    }
    return '-';
  }
}
```

続いて groovy + spock によるテスト

```groovy:TetroidSpec.groovy
@Grab('org.spockframework:spock-core:0.7-groovy-2.0')

import spock.lang.*

class TetroidSpec extends Specification {
  @Unroll
  def "#input should be #expected-type tetromino."() {
   given:
    Tetroid m = new Tetroid()
   expect:
    m.getName(input) == expected
   where:
    input         | expected
    "12,10,21,11" | 'T'
    "89,99,79,88" | 'T'
    "32,41,43,42" | 'T'
    "27,16,36,26" | 'T'
    "07,17,06,05" | 'L'
    "21,41,31,40" | 'L'
    "62,74,73,72" | 'L'
    "84,94,74,75" | 'L'
    "48,49,57,47" | 'L'
    "69,89,79,68" | 'L'
    "90,82,91,92" | 'L'
    "13,23,03,24" | 'L'
    "43,54,53,42" | 'S'
    "95,86,76,85" | 'S'
    "72,73,84,83" | 'S'
    "42,33,32,23" | 'S'
    "66,57,67,58" | 'S'
    "63,73,52,62" | 'S'
    "76,68,77,67" | 'S'
    "12,11,22,01" | 'S'
    "00,01,10,11" | 'O'
    "68,57,58,67" | 'O'
    "72,62,61,71" | 'O'
    "25,24,15,14" | 'O'
    "24,22,25,23" | 'I'
    "51,41,21,31" | 'I'
    "64,63,62,65" | 'I'
    "49,69,59,79" | 'I'
  }
  @Unroll
  def "#input should not be a tetromino."() {
   given:
    Tetroid m = new Tetroid()
   expect:
    m.getName(input) == expected
   where:
    input         | expected
    "55,55,55,55" | '-'
    "05,26,06,25" | '-'
    "03,11,13,01" | '-'
    "11,20,00,21" | '-'
    "84,95,94,86" | '-'
    "36,56,45,35" | '-'
    "41,33,32,43" | '-'
    "75,94,84,95" | '-'
    "27,39,28,37" | '-'
    "45,34,54,35" | '-'
    "24,36,35,26" | '-'
    "27,27,27,27" | '-'
    "55,44,44,45" | '-'
    "70,73,71,71" | '-'
    "67,37,47,47" | '-'
    "43,45,41,42" | '-'
    "87,57,97,67" | '-'
    "49,45,46,48" | '-'
    "63,63,52,72" | '-'
    "84,86,84,95" | '-'
    "61,60,62,73" | '-'
    "59,79,69,48" | '-'
    "55,57,77,75" | '-'
  }
}
```

実装の方針は、今まで出ているものと多少なりとも変えようということで、適当にソートして最初に来たものを (3, 3) にするという正規化をするというもの。
(0,0) にすると負の値が出てしまうので、それを避けるために (3, 3) で。
回転はやめて、xyの入れ替えだけを実装。
あと、せっかく java なのでクラスとか継承とか使ったり。（とは、ruby の時には思わない不思議）
java 側は 138行。
この方針でももう少し短くなる気がしてならない。

spock の方は使いやすくて素晴らしいんだが、何をどうしたらこのようなものが実現できるのかさっぱりわからない。こういうライブラリが書けるようになるためには何を読めばいいんだろう。

しかし。シンタックスハイライト可能な言語に groovy が入ってないの何とかなりませんか？＞中の人

---

追記：上記のように書いたところ、下記コメントのとおり、対応してくださいました！
というわけで、上にあるソースも "groovy:" に変更。