これはG*Advent Calendar 2014の19日目の記事となります。実はQiitaへは初投稿です。
#はじめに
インスタンスの内容を表示する際に、綺麗に整形して表示したい事はありませんか?私はあります。
綺麗に整形して表示することをpretty-printというのですが、今回はこれをGroovyで実装してみたいとおもいます。なお、時間の関係上全てを網羅している訳では無いので、モドキです。
#すでに実装あるんじゃないの?
Pretty-printは昔から良く有るネタですので、すでに実装があるんじゃないの?とまずは考えますよね。
Googleで調べた所、次の物を見つけました。
https://github.com/sjhorn/general-utils/blob/master/PrettyPrinter.groovy
Nabbleのこちらを参考にしたものですが、コードが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(' ')
することもできます。
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が行われます。私もLTを行う予定です。