テキスト処理入門
前の課で文字列をファイルに書いたり読んだりが、とりあえずは出来るようになりました。
あとは保存する文字列を作ったり、保存されている文字列を読んでどうにかすれば、 実はほとんど全てのデータを保存する事が出来ます。
ここでは目的の文字列を作ったり、文字列から目的のデータに変換したりする処理、「テキスト処理」を学びます。
テキスト処理は一大分野で、世の中には仕事のほとんどがテキスト処理だ、というプログラマもそれなりにいるくらいです。 ここではとりあえず当面の用が足せるくらいの理解を目指します。
なお、公式ドキュメントにここに載せてないのもあるので、必要になったらそちらも参照したい。 英語だけど自動翻訳とか使って頑張れ。
以下ではとりあえず使いそうなString型のメソッドを紹介していきます。
最終的にやりたい事
個々のメソッドを見ていく前に、そもそもどういう事をやりたくなるのか、という事から説明してみます。
とりあえずやりたい事としては、ListViewに表示するアイテムを、1行1アイテムで保存していくような事がやりたい。 例えば、以下のようなdata classとリストがあったとする。
data class Post(val content: String, val created: Date)
val mlist = mutableListOf<Post>()
このmlistをファイルに保存して、次回起動時にこのファイルから読み出してリストをもう一回作りたい、とする。
こういう時に、各要素を1行で保存する、というのが一番カンタンな保存方法となる。 DateはgetTimeで数字に出来る事を思うと、カンマで区切った以下のような文字列なら、このデータを保存出来そう。
1691126681002,これは一行目のアイテムです
1691137849935,これは二行目のアイテムです
1691189379291,これは三行目です。別にどんな文字列でもいいですが、このフォーマットだと改行は入れられない。
こんな風に、1行につき一つの要素が対応するような文字列を作ってファイルに保存したり、ファイルから取り出して1行ずつ見て言ってPostオブジェクトに変えていったりしたい。
こういう感じの事が出来るようになるのを目標に、いろいろ機能を見ていきます。
ちなみにこういう風に、最初に数字を置いてカンマで区切って次に文字を置く、みたいなルールを「フォーマット」と呼びます。日本語に訳すと書式なんでしょうけれど、みんなカタカナでフォーマットと呼んでますね。
文字で分割するsplit
まずは文字列を適当な文字で分割するsplitというメソッドを見ていきます。 splitは分割する文字列を渡すと、それで分割した結果のListが返ってきます。
content.split(",")
で、カンマで分割した結果のリストを返します。
この区切る文字を「セパレータ」と呼んだりもします。
また、良くある事として、1行ずつに分ける、というのもこれを使います。行で分割するには、行の終わりに必ず\n
があるのを利用して、
\n
で分割すれば良い。
ダブルクオート三つはraw stringという機能で、改行などを含める文字列を作るのに使えるものでした。(文字列入門参照)
以下のように書いても全く同じ意味です。
val content = "これは一行目です\nこれは二行目です\nこれは三行目です\n"
こういう風に書くのは面倒なので、ファイルの中身と同じようなテストデータを用意する時には今後はこのraw stringを使っていきます。 前回のTextFileLib.readTextでファイルを読みだした結果はだいたいこのような文字列となるので、 この文字列を自由に扱えたらファイルの中身も自由に扱えます。
さて、0番目から始まるのはいいとして、最後に1行、空の行が出来てしまいます。 これは最後が改行で終わっているからです。
以下のように最後の改行を取ると、ちゃんと3行だけになります。
なお、この最後に改行だけがある場合に実際にどう対処すれば良いか、というのは、後のtrimEnd
を使えば良いです。
limitでsplitした結果の数を指定
さて、先ほど見たファイルに保存する例を考えます。
1691126681002,これは一行目のアイテムです
1691137849935,これは二行目のアイテムです
1691189379291,これは三行目です。別にどんな文字列でもいいですが、このフォーマットだと改行は入れられない。
1つ目のカンマはいいとして、後ろの文字にカンマが使われるとどうなるでしょう?例えば1行目の「これは」の後にユーザーがカンマを入力した場合を考えると以下のようになります。
1691126681002,これは,一行目のアイテムです
これをsplitすると3つになってしまう。でも本当は、最初の数字とそれ以外の文字の二つにわけたい。
こういう用途では、splitした結果の要素数の最大個数を指定する、というのが出来ます。
limit=
というもので指定します。
以下例を見てみましょう。
limit=2
とあると、その個数までsplit出来たら以後の文字の中にセパレータがあっても無視する、という挙動になります。
関数の引数にlimit=2
などと指定するのは今回が初めての新しい指定方法ですね。
これはしばらくはこのsplitでしか使わないので細かい解説はせずに、
splitだけこういうのがあると覚えておいて先に進んでください。
これを使えば、以下のように各行を処理する事が出来ます。
これで文字列からPostのオブジェクトのリストに戻せそうです。(後でそういう課題をやります)
splitの逆はjoinToString
ファイルから読む場合はsplitして処理します。逆にファイルに保存する場合はリスト最後に一つの文字列につなげる、という事を良くやります。
これまでやったようにvarと+=
でつなげてもいいのですが、この処理は良くあるので専用のメソッドも覚えるに値します。
それがjoinToStringです。
以下のように使います。
要素と要素の間に指定した文字を挟んでつなげてくれます。例えば改行では無くカンマを挟むと以下のようになります。
startsWith, endsWith
文字列が何で始まっているか、何で終わっているか、というので分岐したい事も良くあります。 そういう時につかうのがstartsWithとendsWithです。
まずstartsWithの例を見てみましょう。
指定した文字列で始まっているならtrueを、始まっていないならfalseを返します。
次はendsWithの例。
指定した文字で終わっていたらtrueを、終わっていなければfalseを返します。
例えばファイルの一覧から.txt
で終わるファイルだけを集めたりする時に使います。
課題: .txtで終わるファイルだけを含んだリストを返す、textFileOnly関数を作れ
ヒント: List<String>
を引数にしてList<String>
を返します。mutableListOfのローカル変数を作って、endsWithでチェックしてadd。
文字の位置を探すindexOf
文字の中で、指定された文字がどこにあるかを探すのがindexOfメソッドです。 先頭から何文字目かのインデックスを返します。
見つからないと-1を返します。
次のsubstringと組み合わせるとだいたいなんでも出来ますが、めちゃバグりやすい。
一部を取り出すsubstring
文字列の中の一部を文字列として取り出すのはsubstringメソッドです。 数字一つを指定するとそこより後ろの文字全部を、数字二つを指定すると始まりのインデックスと終わりのインデックスの一つ手前までを(なんで!?って感じだが)返します。
また、rangeを指定する事もできます。以下例を見てみましょう。
indexOfとsubstringを使えばたいていの事は出来るけれど、添字のバグが生まれやすいので、10倍界王拳くらいのつもりで使いましょう。
長さはlength
文字の長さはlengthです。これはメソッドでは無くプロパティ(つまりlength()
では無くlength
)です。
また、substringと組み合わせると後ろの方だけ取る、みたいな事も出来ます。
trim, trimStart, trimEnd
前後の空白とかをカットする。
中にある空白はカットされず、前後のだけがカットされている事に注目。 trim系はあくまで両端をカットするだけで、何かtrim対象じゃない文字が間にあったらその中はもうカットしません。
なお、トリムする文字を指定する事も出来る。
これはダブルクオートじゃなくてシングルクオート("a"
では無く'a'
とする)なのに注意。
理由は文字列じゃなくて文字だからなのだけれど、とりあえずtrimの時はシングルクオートと丸暗記しておけばいいです。
splitの例であった、最後が空行が入っているケースも、これを使うと以下のように出来る。
これは割と基本的なパターンで、
- ファイルから読む
- trimEndで最後の改行を(あれば)取り除く
- splitで行ごとにバラす
というのは今後何度もやる事になるでしょう。
課題: テキストからPostのリストを作ろう
ファイルから読み込むケースをまずはやってみます。 以下のような形式のテキストから、
1691126681002,これは一行目のアイテムです
1691137849935,これは二行目のアイテムです
1691189379291,これは三行目です。別にどんな文字列でもいいですが、このフォーマットだと改行は入れられない。
Postのリストを作ります。
data class Post(val content: String, val created: Date)
- ヒント1: “1691126681002”は大きい数字なので数字にするのにtoInt()では無くtoLong()を使う。
- ヒント2: 1691126681002という数字から対応するDateを作るのはDate(1691126681002)
という事でやってみましょう。
- 最後の改行を(あれば)trim
- 行にsplitでバラす
- 各行をさらにカンマでバラす、limitも忘れずに
- Post型のオブジェクトを作りリストに追加していく
これを行う関数は、parseTextという名前にしましょう。このように文字列からオブジェクトにする事をパースといいます。
課題: Postのリストからテキストを作ろう
今度は逆にファイルに保存する時です。Postのリストからひとつながりの文字列を作ります。フォーマットも先ほどと同じフォーマットにしましょう。
名前はconvertToTextにしますか。convertは変換するとかそういう意味です。
ヒント: まずは1行を1要素とするListを作って、joinToStringしよう。