kotlin でステートマシンを使って行ごとの状態を把握したい。
というか、そんな大げさな話ではない。 ^#\s を見出し行として、それより上がヘッダ、それより下がボディとしているテキストファイルがあったときに、 ヘッダとボディを別々に分けたい、そのときどうしたらいいかという話。
例として、次のようなテキストファイルを考える。
example.txt
date: 2023-04-01
id: c5c7237d-5850-47b8-9dac-a96bd8b934b9
# Lorem Ipsum
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
これを ヘッダとボディにわけたい場合、(副作用がある)コードをとりあえず書いてみる。
main.kts
import java.io.File
import java.io.FileInputStream
enum class NoteContentState { HEADER, BODY }
val titleRegex = "^#\\s".toRegex()
val headerLines = mutableListOf<String>()
val bodyLines = mutableListOf<String>()
FileInputStream(File("example.txt"))
.bufferedReader(Charsets.UTF_8)
.useLines { lineSequences: Sequence<String> ->
var currentState = NoteContentState.HEADER
lineSequences.forEach { line->
val matchResult = titleRegex.find(line)
if( matchResult!=null ){
currentState = NoteContentState.BODY
}
when(currentState){
NoteContentState.HEADER -> headerLines.add(line)
NoteContentState.BODY -> bodyLines.add(line)
}
}
}
println("--- header ---")
println( headerLines.joinToString("\n") )
println("--- body ---")
println( bodyLines.joinToString("\n") )
初期状態では NoteContentState.HEADER にしておいて、先頭行から調べつつ、タイトル行を見つけたら、NoteContentState.BODY 状態に遷移する。 あとはその状態に応じて headerLines か bodyLines に行の値を蓄積させるだけのコード。
ファイル構成の確認:
.
├── example.txt
└── main.kts
実行する。
$ kotlinc -script main.kts
--- header ---
date: 2023-04-01
id: c5c7237d-5850-47b8-9dac-a96bd8b934b9
--- body ---
# Lorem Ipsum
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
できた。 ヘッダとボディをわけたい、という目的を達成させればよいのであれば、機能はしている。
ただ、これはかなり気持ち悪いコード。 副作用を発生させないでこれを記述できないか考えてみた。
それから useLines は Reader から読みとった結果を返すことを期待されているわけだから、そのようにコードしたい。
main2.kts
import java.io.File
import java.io.FileInputStream
enum class NoteContentState { HEADER, BODY }
val updateState: (NoteContentState)->NoteContentState = { currentState->
when(currentState){
NoteContentState.HEADER-> NoteContentState.BODY
NoteContentState.BODY-> NoteContentState.BODY
}
}
val titleRegex = "^#\\s".toRegex()
val (headerLines, bodyLines) = FileInputStream(File("example.txt"))
.bufferedReader(Charsets.UTF_8)
.useLines { lineSequences: Sequence<String> ->
val initialValue = listOf<Pair<NoteContentState,String>>()
val pairs = lineSequences.fold( initialValue, { acc, line->
val currentState = if( acc.size==0 ){ NoteContentState.HEADER } else { acc[acc.size-1].first }
val matchResult = titleRegex.find(line)
if( matchResult!=null ){
(acc + listOf(Pair(updateState(currentState), line)))
} else {
(acc + listOf(Pair(currentState, line)))
}
})
val headerLines = pairs.fold( listOf<String>(), { acc, pair->
when(pair.first){
NoteContentState.HEADER -> (acc + listOf(pair.second))
NoteContentState.BODY -> acc
}
})
val bodyLines = pairs.fold( listOf<String>(), { acc, pair->
when(pair.first){
NoteContentState.HEADER -> acc
NoteContentState.BODY -> (acc + listOf(pair.second))
}
})
Pair(headerLines, bodyLines)
}
println( headerLines.joinToString("\n") )
println( bodyLines.joinToString("\n") )
補足: pairs (pairs: List<Pair<NoteContentState, String>>) の部分で行ごとに HEADER か BODY かのメタ情報を追加している。
コードは長くなったが、とりあえず副作用がなくなったし、 useLines は結果を返すようにしたので、 これで(自己)満足することにする。