Groovyで汎用pretty-print(モドキ)を作成してみる

これは[G*Advent Calendar 2014](http://qiita.com/advent-calendar/2014/gastah)の19日目の記事となります。実はQiitaへは初投稿です。

#はじめに
インスタンスの内容を表示する際に、綺麗に整形して表示したい事はありませんか?私はあります。
綺麗に整形して表示することを[pretty-print](http://en.wikipedia.org/wiki/Prettyprint)というのですが、今回はこれをGroovyで実装してみたいとおもいます。なお、時間の関係上全てを網羅している訳では無いので、モドキです。

#すでに実装あるんじゃないの?
Pretty-printは昔から良く有るネタですので、すでに実装があるんじゃないの?とまずは考えますよね。
Googleで調べた所、次の物を見つけました。
https://github.com/sjhorn/general-utils/blob/master/PrettyPrinter.groovy
Nabbleの[こちら](http://groovy.329449.n5.nabble.com/Elegant-quot-Groovy-quot-Way-to-Pretty-Print-List-Map-Structures-td355608.html)を参考にしたものですが、コードがGroovyっぽく無いですし、リストの最後に区切り文字があったりするので、ちょっと気に入りません。

また、Groovyでは1.8からJSONのpretty-printが実装されていますが、これを利用したJSON用の物をQiitaで見つけました。
http://qiita.com/hina0118/items/2f179b438ea27ab7b093
表示する範囲がJSONであれば、素直にこれを使うのが良いと思います。ただ、これはJSONに準拠した内容の出力しかできないので、Setがあると例外を発生したり、Mapのキーは文字列だったりと、汎用的に使う分にはちょっと足りなかったりします。

#無いなら作ろう
気に入った物が無かった?それなら作りましょう。
今回の実装方針は、次の通りです。

* 時間に限りがあるので、基本的な部分(基本型,List,Map,Set,POGO)は実装するが、ClosureとかExpandoとかは面倒なので実装しない(JsonOutputでは実装されています)
* MOPを使って既存クラスを拡張して実装(カテゴリとかプラグインとかも考えましたが、、、)
* なるべくGroovyの特徴が見える様に実装

#コード
今回作成したコードは、動作確認代わりのサンプルを含めてこの後にあります。
使い方は、インスタンスに対して`prettyPrint()`メソッドを呼ぶと、整形された内容が文字列で取得できます。メソッド引数にインデント文字列を指定`prettyPrint(' ')`することもできます。

```groovy:PrettyPrint.groovy
Object.metaClass {
  //indent
  indent << {String indentString, int indentLevel, boolean skipIndent ->
    (indentString * (skipIndent ? 0 : indentLevel)) + delegate
  }
  //prettyPrint
  prettyPrint << { ->
    delegate.prettyPrint(' ')
  }
  prettyPrint << {String indentString ->
    delegate.prettyPrint(indentString, 0, false)
  }
  prettyPrint << {String indentString, int indentLevel, boolean skipIndent ->
    if (delegate) {
      delegate.indentPrint(indentString, indentLevel, skipIndent)
    }
    else {
      delegate.indent(indentString, indentLevel, skipIndent)
    }
  }
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    Map properties = delegate.getProperties()
    properties.remove('class')
    properties.remove('declaringClass')
    properties.remove('metaClass')
    properties.indentPrint(indentString, indentLevel, skipIndent)
  }
}
Closure indentList = {String indentString, int indentLevel, boolean skipIndent ->
  '['.indent(indentString, indentLevel, skipIndent) + delegate.collectMany {
    [System.lineSeparator() + it.prettyPrint(indentString, indentLevel + 1, false)]
  }.join(',') + System.lineSeparator() + ']'.indent(indentString, indentLevel, false)
}
Iterable.metaClass {
  //indentPrint
  indentPrint << indentList
}
Enumeration.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    delegate.iterator().indentPrint(indentString, indentLevel, skipIndent)
  }
}
Iterator.metaClass {
  //indentPrint
  indentPrint << indentList
}
Map.metaClass {
  //indentPrint
  indentPrint << indentList
}
Map.Entry.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    delegate.key?.prettyPrint(indentString, indentLevel, skipIndent) +
      ': ' + delegate.value?.prettyPrint(indentString, indentLevel, true)
  }
}
Closure indentSimple = {String indentString, int indentLevel, boolean skipIndent ->
  delegate.indent(indentString, indentLevel, skipIndent)
}
Boolean.metaClass {
  //indentPrint
  indentPrint << indentSimple
}
Number.metaClass {
  //indentPrint
  indentPrint << indentSimple
}
Closure indentToString = {String indentString, int indentLevel, boolean skipIndent ->
  delegate.toString().indentPrint(indentString, indentLevel, skipIndent)
}
Character.metaClass {
  //indentPrint
  indentPrint << indentToString
}
CharSequence.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    '"'.indent(indentString, indentLevel, skipIndent) + delegate?.replaceAll('"','\\\\"') + '"'
  }
}
Date.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    delegate.format("yyyy-MM-dd'T'HH:mm:ssZ", TimeZone.getTimeZone("GMT")).indentPrint(indentString, indentLevel, skipIndent)
  }
}
Calendar.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    delegate.getTime().indentPrint(indentString, indentLevel, skipIndent)
  }
}
UUID.metaClass {
  //indentPrint
  indentPrint << indentToString
}
URL.metaClass {
  //indentPrint
  indentPrint << indentToString
}
Enum.metaClass {
  //indentPrint
  indentPrint << {String indentString, int indentLevel, boolean skipIndent ->
    delegate.name().indentPrint(indentString, indentLevel, skipIndent)
  }
}
Closure indentPrimitiveArray = {String indentString, int indentLevel, boolean skipIndent ->
  delegate.toList().indentPrint(indentString, indentLevel, skipIndent)
}
Object[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
boolean[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
byte[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
char[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
short[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
int[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
long[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
float[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}
double[].metaClass {
  //indentPrint
  indentPrint << indentPrimitiveArray
}

//サンプル(Sample)
//List
char ch = 'C'
Date date = new Date(1234567890123)
Calendar cal = Calendar.instance
cal.timeZone = TimeZone.getTimeZone("GMT")
cal.set(year:2011, month:Calendar.DECEMBER, date:25, hourOfDay:10, minute:9, second:8)
UUID uid = UUID.fromString("507e2e2e-bf1e-4ab0-97d3-cf85ea6d4a48")
URL url = new URL('http://groovy-lang.org/')
List q1 = [100, 2.3, 1/2, true, null, '\"abc\"', ch, date, cal, uid, url]
String a1 = '''[
 100,
 2.3,
 0.5,
 true,
 null,
 "\\\"abc\\\"",
 "C",
 "2009-02-13T23:31:30+0000",
 "2011-12-25T10:09:08+0000",
 "507e2e2e-bf1e-4ab0-97d3-cf85ea6d4a48",
 "http://groovy-lang.org/"
]'''.replaceAll('\n', System.lineSeparator())
//println q1.prettyPrint()
assert q1.prettyPrint() == a1
//println q1.iterator().prettyPrint()
assert q1.iterator().prettyPrint() == a1
//Map
Map q2 = [1.0:'this', 2.0:null]
String a2 = '''[
 1.0: "this",
 2.0: null
]'''.replaceAll('\n', System.lineSeparator())
//println q2.prettyPrint()
assert q2.prettyPrint() == a2
//Map & List
def q3 = [foo:1, bar:2, baz:["日本語", 'C:\\Windows', "3"], zoo:[on:"on", off:"off", 1:2]]
String a3 = '''[
 "foo": 1,
 "bar": 2,
 "baz": [
  "日本語",
  "C:\\Windows",
  "3"
 ],
 "zoo": [
  "on": "on",
  "off": "off",
  1: 2
 ]
]'''.replaceAll('\n', System.lineSeparator())
//println q3.prettyPrint()
assert q3.prettyPrint() == a3
//POGO
class Name {
  String firstName
  String familyName
}
Name q4 = new Name(firstName:'Yasuharu', familyName:'Hayami')
String a4 = '''[
 "firstName": "Yasuharu",
 "familyName": "Hayami"
]'''.replaceAll('\n', System.lineSeparator())
//println q4.prettyPrint()
assert q4.prettyPrint() == a4
//Arrays - boolean[]
def q5 = [true,false] as boolean[]
String a5 = '''[
 true,
 false
]'''
//println q5.prettyPrint()
assert q5.prettyPrint() == a5
//Arrays - char[]
def q6 = ['1','2','3'] as char[]
String a6 = '''[
 "1",
 "2",
 "3"
]'''
//println q6.prettyPrint()
assert q6.prettyPrint() == a6
//Arrays - int[],byte[],short[],long[]
def q7 = [[1,2,3] as int[],[1,2,3] as byte[],[1,2,3] as short[],[1,2,3] as long[]]
String a7 = '''[
 1,
 2,
 3
]'''
//println q7.prettyPrint()
q7.each {assert it.prettyPrint() == a7}
//Arrays - float[],double[]
def q8 = [[1.0,2.0,3.0] as float[],[1.0,2.0,3.0] as double[]]
String a8 = '''[
 1.0,
 2.0,
 3.0
]'''
//println q8.prettyPrint()
q8.each {assert it.prettyPrint() == a8}
//Arrays - BigDecimal[]
def q9 = [1/2, 3/2, 5/2] as BigDecimal[]
String a9 = '''[
 0.5,
 1.5,
 2.5
]'''
//println q9.prettyPrint()
assert q9.prettyPrint() == a9
//Enum
enum EnumNumber {
  First,
  Second,
  Third
}
def q10 = EnumNumber.values()
String a10 = '''[
 "First",
 "Second",
 "Third"
]'''
//println q10.prettyPrint()
assert q10.prettyPrint() == a10
```

#コードの説明
文字列を整形する為に、各クラスには次のメソッドを追加しています。

* `prettyPrint()`:文字列取得の入り口

* `prettyPrint(String indentString)`:インデント文字列を指定した入り口

> indentString:インデント文字列

* `prettyPrint(String indentString, int indentLevel, boolean skipIndent)`:インデントレベルも指定した入り口

> indentString:インデント文字列
> indentLebel:インデントレベル
> skipIndent:trueの場合、先頭のインデントをスキップする

* `indentPrint(String indentString, int indentLevel, boolean skipIndent)`:整形部分の本体(括弧や改行等の処理が含まれる)

> indentString:インデント文字列
> indentLebel:インデントレベル
> skipIndent:trueの場合、先頭のインデントをスキップする

* `indent(String indentString, int indentLevel, boolean skipIndent)`:インスタンスの`toString()`をインデントして文字列の生成

> indentString:インデント文字列
> indentLebel:インデントレベル
> skipIndent:trueの場合、先頭のインデントをスキップする

#Groovyの特徴
今回、Groovyの特徴がなるべく見える様な実装を心がけました。以下に挙げます。

* セミコロンレス
* MOP(metaClass)を使った既存クラスへのメソッドの追加
* クロージャ(Closure)を使ったメソッドの記述とクロージャの共有
* `<<`演算子を使ったメソッドの追加
* `*`演算子を使った文字列の繰り返し
* コレクションの操作メソッド(`collectMany()`,`join()`)
* `return`文の除去
* サンプル部分:文字列の複数行指定(`'''〜'''`)
* サンプル部分:`assert`が便利になるPower Assert

#終わりに
本日12/19の19:00から、品川のNTTソフトウェア本社でJGGUG主催の[G*ワークショップZ](http://jggug.doorkeeper.jp/events/17692)が行われます。私もLTを行う予定です。