今回のコードライティングがあまりに易しく力が抜けてしまったやに思われますので、ここで引き締めて更に本格的なコードライティングを行いましょう。
- 不完全なCSVサポート
- 「カンマで区切りゃ良い」ってもんじゃない ── 一般的なCSV形式
- CSVデータの正しい書き出し
- CSVデータの正しい読み込み(1) ── 現状の把握
- CSVデータの正しい読み込み(2) ── 様々なCSVデータの例
- CSVデータの正しい読み込み(3) ── オートマトンを援用した実装
- ちゃんと動きますか?(再)
- 【補足】テキスト版「getRowCSV:」 ── ユーザのための実装
不完全なCSVサポート
まず、example1を実行して以下のような工程表を拵えてください。
工程名 | 開始日 | 終了日 | 担当者 |
分析,設計 | 2012/07/20 | 2012/07/31 | 太郎,次郎 |
実装 | 2012/08/01 | 2012/08/15 | 三郎 |
このデータを一旦ファイルに保存し、アプリケーションを終了してください。もう一度アプリケーションを足しあげて、今保存したファイルを開いてみると...
これはマズイ...orz
「カンマで区切りゃ良い」ってもんじゃない ── 一般的なCSV形式
保存したCSVファイルの中身を見てみると...
分析,設計,2012/07/20,2012/07/31,太郎,次郎
実装,2012/08/01,2012/08/15,三郎
...困りましたね、データの中にカンマ「,」が含まれるわけですから、CSVファイルに出力されて当然なのですが、そのカンマが区切り文字として認識されてしまったようです。
この4列のデータをCSV形式に直すと、以下のように単純なカンマ区切りの文字列として得られます。
【CSV形式】 実装,2012/08/01,2012/08/15,三郎
では、次の例はどうでしょう。
分析,設計 | 2012/07/20 | 2012/07/31 | 太郎,次郎 |
【誤ったCSV形式】 分析,設計,2012/07/20,2012/07/31,太郎,次郎
【正しいCSV形式】 "分析,設計",2012/07/20,2012/07/31,"太郎,次郎"
データの中にカンマを含む際、正しいCSV形式では引用符「"」で両端を囲みます。つまり、引用符に囲まれたカンマは単なるデータとして扱い、そうでないカンマは区切り文字として扱うのです。
加えて言えば、カンマだけでなく改行文字・タブ文字・空白文字を含む場合も、引用符で囲む場合があります。
さらに加えて、データの中に引用符「"」が含まれている場合には、「""」に置換することでエスケープします。なので、下のような例なら...
秋の"京都",仏閣巡り | 2012/11/23 | 2012/11/25 | 四郎 |
【正しいCSV形式】 "秋の""京都"",仏閣巡り",2012/11/23,2012/11/25,四郎
上のように変換されることになります。
CSVデータの正しい書き出し
正しいCSV形式を理解したところで、実際のプログラムに反映させてゆきましょう。
修正するのは、putRowCSV:with:ですが、少しゴタゴタした処理なのでcsvString:というメッセージを新しく拵えて、そちらで処理を施しましょう。
- 対象のマス目(セル)のデータに、カンマ・改行文字・タブ文字・空白文字・引用符が含まれているか検査する。
- 含まれておれば、そのデータを引用符で囲んでaStreamに流す。ただし「"」は「""」に置換する。
- 含まれていなければ、そのデータをそのままaStreamに流す。
【putRowCSV:with:の実装】
putRowCSV: aStream with: aRow
aRow do:
[:each |
| aString |
aString := each isString ifTrue: [each] ifFalse: [each printString].
aStream nextPutAll: aString]
separatedBy: [aStream nextPut: $,].
aStream cr
【新しいメッセージcsvString:の実装】
csvString: aString
さて、これでCSV形式の正しい書き込みができるようになったはず。先ほどのデータをもう一度保存してみましょう。
"分析,設計",2012/07/20,2012/07/31,"太郎,次郎"
実装,2012/08/01,2012/08/15,三郎
ちゃんと正しい形式で保存できましたね!!
CSVデータの正しい読み込み(1) ── 現状の把握
もう一度アプリケーションを立ち上げて、正しく保存できたCSVデータを開いてみましょう。(ダメ元で。)
やっぱりダメでした。CSVのデータを読み込んでいるメッセージgetRowCSV:を再度読んでみましょう。
カンマ | データ(セル)の区切りなので、バッファの内容物をコレクションに追加して、バッファを空にする。 |
カンマ・改行 以外 | データ(セル)内の文字列が続いているので、バッファに追加する。 |
改行 | 1レコード(1工程)のデータを完全に読みきったので、バッファの中身をコレクションに入れてから処理終了。 |
【現在のCSVデータ読み取り処理の遷移】
【パスタ,京都,肉,ほげ\n】
aCharacter =
aBuffer =
aCollection = { }
CSVデータの正しい読み込み(2) ── 様々なCSVデータの例
実際に存在しうる様々なCSVデータの例をご覧に入れましょう。その上で、CSV形式の正しい読み込み方について考えてゆきます。
まずは普通のデータ
あいうえお,かきくけこ,さしすせそ
カンマ混じりのデータ
あいうえお,",か,き,く,け,こ,",さしすせそ
引用符「"」混じりのデータ
あいうえお,"""か""き""く""け""こ""",さしすせそ
改行混じりのデータ。青字の「\n」は改行文字。
あいうえお,"かきく\nけこ",さしすせそ
空セルのあるデータ
あいうえお,,さしすせそ
全てが空セルのデータ
,,
無茶苦茶なデータ(これでも正しいCSV形式)
"あ""い,う\nえお","かきくけこ",
さあ、これらを上手く区切る解析器を拵えましょう。
CSVデータの正しい読み込み(3) ── オートマトンを援用した実装
いきなりですが、CSV形式を読み込む際のオートマトンを描いてみましょう。ちょうど皆さんが履修している「言語オートマトン」を活かす機会がやってきたのです。
以下の手順に従って、オートマトンの図(状態遷移図)を描いてみてください。
- 初期状態を用意する。セルデータを読み込む直前の状態。
- 既に用意した状態のうち1つに注目して、何らかの文字が入力された場合の遷移先状態を用意する。
- 新しい状態が作られなるまで2.を繰り返す。
いきなりですが、CSV形式を読み込む際のオートマトンを描いてみましょう。ちょうど皆さんが履修している「言語オートマトン」を活かす機会がやってきたのです。
完成版オートマトンの画像
オートマトンが完成したところでプログラミングを始めましょう。メッセージgetRowCSV:を修正します。
【getRowCSV:の実装】
getRowCSV: aStream
| aCollection aBuffer aBoolean |
aCollection := OrderedCollection new.
aBuffer := String new writeStream.
aBoolean := true.
[aStream atEnd not and: [aBoolean]] whileTrue:
[| aCharacter |
aCharacter := self getChar: aStream.
aCharacter = Character cr
ifTrue: [aBoolean := false]
ifFalse:
[aCharacter = $,
ifTrue:
[aCollection add: aBuffer contents.
aBuffer close.
aBuffer := String new writeStream]
ifFalse: [aBuffer nextPut: aCharacter]]].
aCollection add: aBuffer contents.
aBuffer close.
^aCollection
これで実装は完了しました。Formatした後にAcceptしてください。
ちゃんと動きますか?(再)
どうでしょう。ひたすら冗長に書き認めたオートマトンでしたが、上手く読み込めてスッキリしたのではないでしょうか。
「オートマトンなんて何に使うねん」という素朴な疑問に答える簡単な例になったと思います。これを機に、講義科目「言語オートマトン」に親しみを覚えてもらえると幸いです。
【補足】テキスト版「getRowCSV:」 ── ユーザのための実装
冒頭で紹介したテキストでは、今回修正したものとは異なるgetRowCSV:が実装されています。そのコードを読んでみましょう。
getRowCSV: aStream
| aCollection aBuffer aBoolean |
aCollection := OrderedCollection new.
aBuffer := String new writeStream.
aBoolean := true.
[aStream atEnd not and: [aBoolean]] whileTrue:
[| aCharacter |
aCharacter := self getChar: aStream.
aCharacter = Character cr
ifTrue: [aBoolean := false]
ifFalse:
[aCharacter = $,
ifTrue:
[aCollection add: aBuffer contents.
aBuffer close.
aBuffer := String new writeStream]
ifFalse:
[aCharacter = $"
ifTrue:
[| aLoop |
aLoop := true.
[aStream atEnd not and: [aLoop]] whileTrue:
[aCharacter := self getChar: aStream.
aCharacter = $"
ifTrue:
[aStream peek = $"
ifTrue:
[aStream next.
aBuffer nextPut: $"]
ifFalse: [aLoop := false]]
ifFalse: [aBuffer nextPut: aCharacter]]]
ifFalse: [aBuffer nextPut: aCharacter]]]].
aCollection add: aBuffer contents.
aBuffer close.
^aCollection
今回修正したものに比べて簡潔に書かれています。が、本来受理できないようなデータを受理してしまうため、CSVのパーサとしては正しくないのです。
例えば...
あいうえお,かき"くけこ",さしすせそ
このような「正しくないCSVデータ」を読んでみると、あたかも正しいCSVデータを読んだかのように振る舞ってしまうのです。
さて、ここで考えるべきは、「ユーザが利用するアプリケーションではどちらの実装が良いのか」ということです。
お役所の手続きのように少しのミスも許されない厳密なアプリケーションが良いですか? 少しのミスなら寛大に受け止めてくれる柔軟なアプリケーションが良いですか?
私達が普段使っているCやJavaのコンパイラは「厳密なアプリケーション」のように見えて、実を言うと「柔軟なアプリケーション」なのです。
プログラマの作るプログラムには沢山のミスが含まれているもの。それをコンパイラにかけようものなら、含まれている沢山のミスを一度に幾つも指摘してくれますね。
[1]
$ gcc test.c
t.c: In function 'main':
t.c:5: error: expected ';' before 'printf'
t.c:7: error: expected ';' before 'printf'
$ cat test.c
#include <stdio.h>
int main() {
printf("1");
printf("2") ←セミコロンが抜けているので、これ以降の青文はプログラムとして正しく読めないはず。
printf("3");
printf("4") ←なのに、この行のセミコロン抜けも指摘してくれている。なぜ...
printf("5");
return 0;
}
ユーザの利便性のために厳密性を犠牲にしなければならない場合があります。ソフトウェア開発は、ソフトウェアを使うユーザが存在して初めて成り立つもの。
設計に「ユーザの存在」を組み込んで、どのような仕様が望まれるのかを正しく検討する必要があるのです。間違っても開発者主体のソフトウェア開発を行なわないように。
[1] 参考文献「はじめてのコンパイラ ─ 原理と実践」 宮本衛市 2007 森北出版 ISBN:9784627817210