SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

StateパターンでCSVを読む


  • X ポスト
  • このエントリーをはてなブックマークに追加

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から起こした状態遷移図を示します。

図5 CSVの状態遷移図
図5 CSVの状態遷移図

 なんかあっさりと示しましたけど、状態と事象を抽出してこの図を描き上げることがすなわち設計のキモとなります。状態遷移図が描ければ八割がた完成したも同然。

 状態遷移図を表の形式に変換したものが次に示す状態遷移表です。状態/事象を行/列に並べ、各マスにはそれぞれの状態/事象の組に応じた活動(アクション)と遷移先(次の状態)を記述します。この状態遷移表が実装の拠り所となります。

図6 状態遷移表
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_ << "&amp;";  break;
                          case '<'  : stream_ << "&lt;";   break;
                          case '>'  : stream_ << "&gt;";   break;
                          case '"'  : stream_ << "&quot;"; 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を食わせたら:

図7 生成されたHTML(screen-shot)
図7 生成されたHTML(screen-shot)

 こんなの吐いてくれました。ちゃんと動いてくれてるみたいですよ♪

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/5531 2010/12/23 19:10

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング