StateパターンのCSV読み込みへの適用
では本題である「CSVの読み込み」に取り掛かりましょうか。CSVファイルから一文字ずつ順に読み込んでfieldあるいはrecordの切れ目を見つけるのですから、切れ目を判断するのに必要な文字が読み込まれることが事象(イベント)となります。要するにfieldの切れ目であるカンマ、そしてrecordの区切りである復帰と改行ですね。それともう一つ、escapeされた状態(escaped)とそうでない状態(nonescaped)とを切り替える二重引用符が読み込まれることも事象となります。これら事象を引き起こす文字をそれぞれCM(カンマ)/CR(復帰)/LF(改行)/DQ(二重引用符)、その他の文字をOT(その他)と表記します。
つぎに状態。状態はnonescaped,escapedだけじゃありません。nonescaped状態でCRを読んだとき、次の文字がLFか否かでアクション/遷移先が変化します。さらにescaped状態でDQを読んだとき、次の文字がDQか否かで異なるアクション/遷移先となりますね。状態は以下の4つ:
- nonescaped : escapeされていない("…"の外側)
- escaped : escapeされている ("…"の内側)
- afterCR : (nonescape中の)CR直後
- afterDQ : (escape中の)DQ直後
そんなわけでCSVの読み込みには4つの状態と5つの事象それぞれの組み合わせ(4x5=20通り)に対して正しく反応せにゃなりません。前述のStateパターンを用いないPenと同様の実装だと4分岐するswitch文が5か所に現れること(あるいはswitchの二段重ね)になることでしょう。Stateパターンなら基底クラスStateから導出した4つのクラスがそれぞれ5つのメソッドを持ちます。基底クラスStateでは5つのメソッドを純粋仮想(pure virtual)関数としているので実装の漏れは起こりえません(定義をサボるとコンパイルエラー)し、それぞれが個別の関数であるためswitch内で一緒くたになるよりそれぞれの「キレ」が良くなります。
さてと、ルールを少々ユルくしたRFC4180から起こした状態遷移図を示します。
なんかあっさりと示しましたけど、状態と事象を抽出してこの図を描き上げることがすなわち設計のキモとなります。状態遷移図が描ければ八割がた完成したも同然。
状態遷移図を表の形式に変換したものが次に示す状態遷移表です。状態/事象を行/列に並べ、各マスにはそれぞれの状態/事象の組に応じた活動(アクション)と遷移先(次の状態)を記述します。この状態遷移表が実装の拠り所となります。
CSVparser state-transition-table |
event | |||||
1: CR | 2: LF | 3: DQ | 4: CM | 5: OT | ||
state | A: nonescaped | 何もしない →B |
バッファに追加 →A |
何もしない →C |
fieldの区切り →A |
バッファに追加 →A |
B: afterCR | バッファに追加 →B |
fieldの区切り record の区切り →A |
CRをバッファに追加 →C |
CRをバッファに追加 fieldの区切り →A |
CRをバッファに追加 バッファに追加 →A |
|
C: escaped | バッファに追加 →C |
バッファに追加 →C |
何もしない →D |
バッファに追加 →C |
バッファに追加 →C |
|
D: afterDQ | 何もしない →B |
バッファに追加 →A |
バッファに追加 →C |
fieldの区切り →A |
バッファに追加 →A |
状態遷移表の各マスに記された活動の実装はContextに定義し、必要に応じてStateからContextに委譲します。Context内には読み込んだ文字を順に溜め込むバッファを置き、fieldの区切りが認識された時点でバッファの中身を吐き出します。バッファの内容すなわちfieldを構成する文字列の吐き出し先として抽象クラスHandlerを用意しました。利用者はHandlerの導出クラスを用意し、fieldの区切り/recordの区切りが検出されたときの動作を定義します。
添付したVisual Studio 2010 solution には C++, C#, VB.NET による実装を詰め込んでおきました。
試運転といきましょうか。CSVを読み込んでHTMLに変換してみましょう。Handlerから導出したHTML変換ハンドラとメインルーチンは以下のとおり:
#include "csv_handler.h" #include "csv_parser.h" #include <iostream> #include <fstream> #include <iterator> #include <algorithm> #include <array> #include <vector> using namespace std; /* * CSV を HTML-table に変換する */ class HTMLHandler : public csv::Handler { vector<string> row_; bool header_; ostream& stream_; public: HTMLHandler(ostream& stream) : stream_(stream) {} private: virtual void initialize() { stream_ << "<html><body><table border='1'>" << endl; row_.clear(); header_ = true; } virtual void handleField(const std::string& field) { row_.push_back(field); } virtual void handleRecord() { stream_ << "<tr>"; for_each(row_.begin(), row_.end(), [this](const string& field) { stream_ << (header_ ? "<th>" : "<td>"); for_each(field.begin(), field.end(), [&](char ch) { switch ( ch ) { case '\r' : break; case '\n' : stream_ << "<br />"; break; case '&' : stream_ << "&"; break; case '<' : stream_ << "<"; break; case '>' : stream_ << ">"; break; case '"' : stream_ << """; break; default : stream_ << ch; break; } }); stream_ << (header_ ? "</th>" : "</td>"); }); stream_ << "</tr>" << endl; row_.clear(); header_ = false; } virtual void terminate() { stream_ << "</table></body></html>" << endl; } }; int main() { ofstream out("output.html"); HTMLHandler handler(out); csv::Parser parser(&handler); ifstream in("input.csv", ios::binary); parser.initialize(); for_each(istreambuf_iterator<char>(in), istreambuf_iterator<char>(), [&](char ch) { parser.drive(ch); }); parser.terminate(); };
でき上がったアプリケーションにCSVを食わせたら:
こんなの吐いてくれました。ちゃんと動いてくれてるみたいですよ♪