LoginSignup
1
2

More than 5 years have passed since last update.

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

Posted at

これは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(' ')することもできます。

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が行われます。私もLTを行う予定です。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2