一見して良く分からないタイトルを付けましたが、もともとの問題は、次のものでした。
CSV ファイルを読み込み、各行ごとに何らかの処理を行い、 CSV ファイルを出力したい。
このようなプログラムを作成する必要がありました。もちろん、この問題に直接答えるプログラムを作成することは難しくありません。しかし、可能な限り汎用となるモデルを作っておきたいと考えました。
そこで次のような条件を想定しました。
以下では複数のインターフェース、複数のクラスが紹介されています。さて、この問題をどのように解決したでしょうか?
この章で紹介しているプログラムのモデル図を最初に示します。
図にあります Converter は、以下で登場してくるクラスの名前です。このプログラムで中心的な役割を果たすことになります。図中の2つの円筒形のものは、データの格納場所を表します。左の円筒形は読みこんだデータの格納用に使用し、
右の円筒形はデータの書き出し用に使用します。このデータの格納には、標準クラスの DataTable クラスを使用します。さて、中心となる Converter クラスは、次の3つの処理が行われる場所を提供します。
あとは、このモデルをどのようにプログラムで実現するかがテーマになります。 Converter クラスはこの処理を提供する場となりますが、処理自体にはかかわらないようにする必要があります。
ここでは、2つのインターフェースといくつかのクラスを紹介します。
クラスはやや複雑ですから、ここでリストにしておきたいと思います。
一覧にあるように、読み込みと書き出しのために複数のクラスが用意されています。このクラスは自由に組み合わせて利用することができます。例えば、Excel ファイルから読み込み、 CSV ファイルに書き出すといったことが可能です。 一覧には載せていませんが、データベースから直接読み込むためのクラス、PDF ファイルから読み込むためのクラス、さらにはデータベースに直接書き込むためのクラスも用意してあります。もちろん、自由に組み合わせることができるようになっています。
モデル図にある、入力① に対応したインターフェースが IReadable です。詳細なコメントを付けてありますので、理解は難しくないと思います。
読み込み際しては、前処理と処理本体の2つを定義できるようにしています。何らかの前処理が必要な場合は、実装してください。また、それぞれの処理はラムダ式を利用しています。インターフェースを実装したクラス毎に、異なるパラメータを設定することもできるようにしてあります。
1 public interface IReadable
2 {
/// <summary>
/// 読み込むデータを格納した名前(ファイル名など)を返す
/// </summary>
3 String GetName();
/// <summary>
/// 読み込むデータを格納した名前(ファイル名など)を設定する
/// </summary>
4 void SetName(String name);
/// <summary>
/// 処理結果を返す
/// </summary>
5 String GetError();
/// <summary>
/// 読み込みの前処理のラムダ式を返す
/// 第一引数が読み込んだデータを格納するためのデータテーブル
/// </summary>
6 Func<DataTable, bool> PreReadLambda();
/// <summary>
/// 読込み結果をデータテーブルに格納するためのラムダ式を返す
/// 第一引数が読み込んだデータを格納するためのデータテーブル
/// </summary>
7 Func<DataTable, bool> ReadLambda();
/// <summary>
/// 各種のパラメータを渡すメソッド
/// </summary>
/// <param name="args"></param>
/// <returns>引数のパラメータ配列が正しくない場合に、false を返す</returns>
8 bool SetParameters(Object[] args);
9 }
以下は、簡単な説明です。
3~4行目 | Getter/Setter 用のメソッドです |
5行目 | 読み込み処理が失敗した場合の失敗理由を表すメッセージを返します |
6行目 | 読み込み処理を開始する前の処理が必要な場合に呼び出されるラムダ式を返します |
7行目 | 読み込み処理の本体となるラムダ式を返します |
8行目 | 読み込み処理に必要なパラメータを渡すために使用します。パラメータの数やその役割・順番は呼び出し側と受けて側に任されます |
モデル図にある、変換②と出力③ に対応したインターフェースが IWritable です。このインターフェースは、モデル図の2つの処理に対応していますが、 IReadable と相似形になっていますので、理解は難しくないと思います。
もちろん、モデル図にあるように変換②と出力③に対応した2つのインターフェースを用意することも可能ですが、それだけの必要を感じませんでしたので、このようにしました。
1 public interface IWritable
2 {
/// <summary>
/// データの書き出し先の名前(ファイル名など)を返す
/// </summary>
3 String GetName();
/// <summary>
/// データの書出し先の名前(ファイル名など)を設定する
/// </summary>
4 void SetName(String name);
/// <summary>
/// 処理結果を返す
/// </summary>
5 String GetError();
/// <summary>
/// 読み込んだデータを格納したデータテーブルを、書出し用のデータテーブルに変換するラムダ式を返す
/// 第一引数が読み込んだデータを格納したデータテーブル
/// 第二引数が書出し用のデータテーブル
/// </summary>
6 Func<DataTable, DataTable, bool> PreWriteLambda();
/// <summary>
/// 書出し用のデータテーブルの内容を出力するためのラムダ式を返す
/// 第一引数が書出し用のデータテーブル
/// </summary>
7 Func<DataTable, bool> WriteLambda();
/// <summary>
/// 各種のパラメータを渡すメソッド
/// </summary>
/// <param name="args"></param>
/// <returns>引数のパラメータ配列が正しくない場合に、false を返す</returns>
8 bool SetParameters(Object[] args);
9 }
以下は、簡単な説明です。
3~4行目 | Getter/Setter 用のメソッドです |
5行目 | 書き出し処理が失敗した場合の失敗理由を表すメッセージを返します |
6行目 | 書き出し処理を開始する前の処理が必要な場合に呼び出されるラムダ式を返します |
7行目 | 書き出し処理の本体となるラムダ式を返します |
8行目 | 書き出し処理に必要なパラメータを渡すために使用します。パラメータの数やその役割・順番は呼び出し側と受けて側に任されます |
このモデルの中心となるクラスが Converter クラスになります。このクラスは処理の場を提供することが役目になります。必要に応じて、IReadable インターフェースを実装したクラスや IWritable インターフェースを実装したクラスを参照しますが、 その場合も、インターフェースで定義されているメソッドだけを介して操作することになります。
1 public class Converter
2 {
// 読出し用データテーブルへ読み込むためのオブジェクト
3 protected IReadable reader;
// 書込み用データテーブルから書き出すためのオブジェクト
4 protected IWritable writer;
// 処理結果
5 public String Result
6 {
7 get; private set;
8 }
// 読込み用データテーブル
9 public DataTable InTable
10 {
11 get; protected set;
12 }
13 public int InCount
14 {
15 get
16 {
17 return InTable.Rows.Count;
18 }
19 }
20 public int OutCount
21 {
22 get
23 {
24 return OutTable.Rows.Count;
25 }
26 }
// 書出し用データテーブル
27 public DataTable OutTable
28 {
29 get; protected set;
30 }
/// <summary>
/// Mapper オブジェクトを引数とするコンストラクタ
/// </summary>
/// <param name="mapper"></param>
31 public Converter(Mapper mapper) : this(mapper.Reader, mapper.Writer) { }
/// <summary>
/// IReader, IWriter インターフェースを引数とするコンストラクタ
/// </summary>
/// <param name="reader">IReader インターフェースを実装したオブジェクト</param>
/// <param name="writer">IWriter インターフェースを実装したオブジェクト</param^>
32 public Converter(IReadable reader, IWritable writer)
33 {
34 this.reader = reader;
35 this.writer = writer;
36 Clear();
37 }
38 public void Clear()
39 {
40 Result = String.Empty;
41 InTable = new DataTable();
42 OutTable = new DataTable();
43 }
/// <summary>
/// 変換処理の実施
/// </summary>
44 public bool Run()
45 {
46 if (!Read())
47 return false;
48 if (InTable.Rows.Count == 0)
49 {
50 Result = String.Format("{0}データが存在しません。", reader.GetName());
51 return false;
52 }
53 return Write();
54 }
/// <summary>
/// 自分自身の書出しテーブルのコピーを、引数の読込みテーブルに連結処理する
/// </summary>
/// <param name="conv"></param<
55 public void Connect(Converter conv)
56 {
57 conv.InTable = OutTable.Copy();
58 }
/// <summary>
/// 読出し用 CSV ファイルからデータテーブルに読み出す
/// </summary>
/// <returns>処理に成功したかどうか</returns>
59 public virtual bool Read()
60 {
61 if (reader.PreReadLambda() != null)
62 {
63 if (! reader.PreReadLambda()(InTable))
64 {
65 Result = String.Format("{0}読込み前処理に失敗しました。\r\n{1}",
66 reader.GetName(),
67 reader.GetError());
68 return false;
69 }
70 }
71 if (! reader.ReadLambda()(InTable))
72 {
73 Result = String.Format("{0}読込み処理に失敗しました。\r\n{1}",
74 reader.GetName(),
75 reader.GetError());
76 return false;
77 }
78 return true;
79 }
80 public virtual bool Write()
81 {
82 if (writer.PreWriteLambda() != null)
83 {
84 if (! writer.PreWriteLambda()(InTable, OutTable))
85 {
86 Result = String.Format("{0}書出しの前処理に失敗しました。\r\n{1}",
87 writer.GetName(),
88 writer.GetError());
89 return false;
90 }
91 }
92 if (OutTable.Rows.Count == 0)
93 {
94 Result = String.Format("{0}書出し用のデータがありません", writer.GetName());
95 return false;
96 }
97 if (!writer.WriteLambda()(OutTable))
98 {
99 Result = String.Format("{0}書出し処理に失敗しました。\r\n{1}",
100 writer.GetName(),
101 writer.GetError());
102 return false;
103 }
104 return true;
105 }
106 }
9~12行目 | 読みこんだデータを管理するための DataTable オブジェクトです |
27~30行目 | 書き出し用のデータを管理するための DataTable オブジェクトです |
31行目 | Mapper クラスを指定したコンストラクタです。Mapper クラスの説明を参照してください |
32~37行目 | 読み込み用オブジェクトと書き出し用オブジェクトを指定したコンストラクタです |
44~54行目 | 読み込み処理と書き出し処理を行います |
55~58行目 | 連結動作を可能にします。説明は後述します |
59~79行目 | 読み込み処理を行います |
80~106行目 | 読み込みデータの変換と書き出し処理を行います |
Converter クラスの中で、分かりにくいのは Connect() メソッドだと思います。これは、「入力-変換-出力」の処理を多段に連結するためのものです。前段の出力結果を、次段の入力としてそのまま利用したい場合に使用します。
次のプログラム片をご覧ください。
この連結の段数には制限はありません。何段にも連結することができます。
1 Mapper mapper1 = new MyMapper1();
2 mapper1.InputPath = "input.csv";
3 mapper1.OutputPath = "output.csv";
4 Converter obj1 = new Converter(mapper1);
5 if (!obj1.Run())
6 {
// エラー処理
7 }
8 Mapper mapper2 = new MyMapper2();
9 mapper2.OutputPath = "output2.csv";
10 Converter obj2 = new Converter(mapper2);
11 obj1.Connect(obj2);
12 if (!obj2.Write())
13 {
// エラー処理
14 }
1行目 | 前段で使用する Mapper オブジェクトです |
2~3行目 | 読み込み用ファイル名と書き出し用ファイル名を指定します |
4行目 | 前段で使用する Converter オブジェクトです |
5~7行目 | 前段の処理を実行します |
8行目 | 後段で使用する Mapper オブジェクトです |
9行目 | 後段では読み込み処理は行いませんので書き出し用ファイル名を指定します |
10行目 | 後段で使用する Converter オブジェクトです |
11行目 | 前段の Converter に後段の Converter を連結します |
12行目 | 後段の書き出し処理を実行します。この時読み込み処理は不要ですので Run() メソッドを呼び出してはいけません |
読み込みに使用するオブジェクトと書き出しに使用するオブジェクトのペアを管理するためのクラスです。実は、このクラスは「入力-変換-出力」の処理だけからすると、不要にすることができます。
このクラスを導入した理由は、「入力-変換-出力」処理を行うオブジェクトをリフレクションを使用して、動的に自由に生成したいためです。
読み込み用と書き出し用のペアに関する情報を1つのクラスに閉じ込めておけば、そのクラス名を指定することで動的に必要なオブジェクトを生成できるようになります。ファクトリメソッドを実装するためには、この Mapper クラスが役に立ちますね。
1 public class Mapper
2 {
3 static public bool Copy(DataTable from, DataTable to)
4 {
5 DataRow newRow;
6 foreach (DataRow row in from.Rows)
7 {
8 newRow = to.NewRow();
9 if (!Copy(row, newRow))
10 return false;
11 to.Rows.Add(newRow);
12 }
13 return true;
14 }
15 static public bool Copy(DataRow from, DataRow to)
16 {
17 if (from.ItemArray.Length != to.ItemArray.Length)
18 return false;
19 foreach (var v in Enumerable.Range(0, from.ItemArray.Length))
20 {
21 to[v] = from[v];
22 }
23 return true;
24 }
25 public IReadable Reader
26 {
27 get; protected set;
28 }
29 public IWritable Writer
30 {
31 get; protected set;
32 }
33 public virtual String GetReaderName() => Reader == null ? "" : Reader.GetName();
34 public virtual void SetReaderName(String name)
35 {
36 if (Reader != null)
37 Reader.SetName(name);
38 }
39 public virtual String GetWriterName() => Writer == null ? "" : Writer.GetName();
40 public virtual void SetWriterName(String name)
41 {
42 if (Writer != null)
43 Writer.SetName(name);
44 }
45 public virtual String GetReadError()
46 {
47 return Reader == null ? "" : Reader.GetError();
48 }
49 public virtual String GetWriteError()
50 {
51 return Writer == null ? "" : Writer.GetError();
52 }
// 読出し用の名前
53 public String InputPath
54 {
55 get
56 {
57 return Reader.GetName();
58 }
59 set
60 {
61 if (Reader != null)
62 Reader.SetName(value);
63 }
64 }
// 書込み用の名前
65 public String OutputPath
66 {
67 get
68 {
69 return Writer.GetName();
70 }
71 set
72 {
73 if (Writer != null)
74 Writer.SetName(value);
75 }
76 }
// 読出し用データテーブルのコラム数
77 public int InCount
78 {
79 get; set;
80 }
// 書出し用データテーブルのコラム数
81 public int OutCount
82 {
83 get; set;
84 }
// 処理結果
85 public String Result
86 {
87 get; protected set;
88 }
89 public Mapper(Reader reader, Writer writer)
90 {
91 internalClear();
92 Reader = reader;
93 Writer = writer;
94 }
95 public virtual void Clear()
96 {
97 internalClear();
98 }
/// <summary>
/// データテーブルの構築用メソッド
/// 処理内容は、以下のもの
/// ・データテーブルから既存の全レコードを削除する
/// ・データテーブルから既存の全コラムを削除する
/// ・新規に、1から count 番までの連番からなるコラムを作成する
/// </summary>
99 public static bool MakeTable(int count, DataTable table)
100 {
102 table.Rows.Clear();
103 table.Columns.Clear();
104 foreach (var e in Enumerable.Range(1, count))
105 {
106 table.Columns.Add(e.ToString(), Type.GetType("System.String"));
107 }
108 return true;
109 }
110 private void internalClear()
111 {
112 InCount = 0;
113 OutCount = 0;
114 }
115 }
処理内容は決して難しくないと思いますので、わかりずらい部分だけを説明します。
3~24行目 | 読み込み用データをそのまま書き出し用にコピーするためのユーティリティメソッドです |
99~109行目 | 読み込み用と書き出し用の DataTable のコラムを作成するためのユーティリティメソッドです。コラムの名前は1からの連番となります |
読み込み用のインターフェース IReadable をデフォルト実装したクラスです。別途読み込み用インターフェースを実装したクラスを定義してもよいのですが、このクラスから派生して利用することを想定しています。
豊富なコメントが付いていますので、特別な説明は不要と思います。
1 public class Reader : IReadable
2 {
3 public virtual String GetName() => Name;
4 public void SetName(String name) => Name = name;
5 public virtual String GetError() => Result;
6 public virtual bool SetParameters(Object[] args) => true;
/// <summary>
/// 読み込んだ結果を格納するデータテーブルのコラム数
/// </summary>
7 public int Count
8 {
9 get; set;
10 }
11 public String Name
12 {
13 get; set;
14 }
15 public String Result
16 {
17 get; protected set;
18 }
19 public Reader() { }
20 public virtual void Clear() { }
/// <summary>
/// 読込み用前処理と読込み処理のラムダ式を返す
/// デフォルトでは、それぞれ DefaultPreRead() と DefaultRead() を返す
/// 必要に応じて PreReadLambda(), DefaultPreRead(), ReadLambda(), DefaultRead() を再定義すること
/// </summary>
21 public virtual Func<DataTable, bool> PreReadLambda() => DefaultPreRead;
22 public virtual Func<DataTable, bool> ReadLambda() =< DefaultRead;
/// <summary>
/// デフォルトで用意されている読込み前処理
/// </summary>
23 protected virtual bool DefaultPreRead(DataTable table) => true;
/// <summary>
/// デフォルトで用意されている読込み処理
/// </summary>
24 protected virtual bool DefaultRead(DataTable table) => true;
25 }
CSV ファイルから読み込むためのクラスです。 Reader クラスから派生しています。CSV ファイルから読み込みたい場合は、必要に応じてこのクラスから派生するか、このクラスをそのまま使用するかを検討してください。
直接 CSV ファイルを操作する処理は、CsvFile クラスを利用しています。このクラスについては、こちらを参照してください。
1 public class ReaderCsv : Reader
2 {
3 protected CsvFile csvFile;
4 public ReaderCsv()
5 {
6 csvFile = new CsvFile();
7 }
8 public override string GetError() => csvFile.Result;
/// <summary>
/// 再定義している、読込み前処理
/// </summary>
9 protected override bool DefaultPreRead(DataTable table)
10 {
11 Clear();
// 読込み用 CsvFile の初期化
12 Initialize(csvFile);
// 読込み用のデータテーブルの構築
13 bool flag = Mapper.MakeTable(Count, table);
14 return flag;
15 }
/// <summary>
/// 再定義している、読込み処理
/// </summary>
16 protected override bool DefaultRead(DataTable table)
17 {
18 bool flag = csvFile.Read(table);
19 if (!flag)
20 Result = csvFile.Result;
21 return flag;
22 }
/// <summary>
/// デフォルトの読込用の CsvFile 初期化処理
/// </summary>
23 protected virtual void Initialize(CsvFile csvFile)
24 {
25 csvFile.Path = Name;
26 }
27 }
固定長のテキストファイルから読み込むためのクラスです。 ReaderCsv クラスから派生しています。固定長テキストファイルを使用する場合は、このクラスから派生する必要があります。
固定長のテキストを読み込むために、 ReaderCsv クラスを利用しているのは、次のアイデアによっています。固定長テキストを、1コラムしかない特殊な CSV ファイルとしてひとまず読み込みます。 読みこんだ後に、複数のコラムに分割すればよいと考えました。その分割に使用するのが GetLimits() メソッドが返す配列になります。そのため、固定長テキストを利用したい場合は、派生クラスを定義し、このメソッドをオーバーライドすることが必ず必要です。
1 public class ReaderConstant : ReaderCsv
2 {
3 protected override void Initialize(CsvFile csvFile)
4 {
5 base.Initialize(csvFile);
6 List<KeyValuePair>String, String>> list = new List<KeyValuePair<String, String>>();
7 list.Add(new KeyValuePair<String, String>("1", "1")); // 1番目のコラムに全体を読み取り、他のコラムは使用しない
8 csvFile.ColumnList = list;
9 }
/// <summary>
/// 再定義している、読込み処理
/// </summary>
10 protected override bool DefaultRead(DataTable table)
11 {
12 if (!base.DefaultRead(table))
13 return false;
14 foreach (DataRow row in table.Rows)
15 {
16 if (!DivideColumns(row))
17 return false;
18 }
19 return true;
20 }
/// <summary>
/// 引数のデータロウの1番目のコラムを複数のコラムに分割する
/// 分割は、GetLimits() メソッドの戻り値をもとに処理する
/// デフォルトでは、分割処理を行わないで true を返す
/// 必要に応じて、サブクラスで GetLimits() を再定義する
/// </summary>
21 protected virtual bool DivideColumns(DataRow row)
22 {
23 String val = (String)row["1"];
24 int len = val.Length;
25 int[] limit = GetLimits();
26 if (limit == null || limit.Length != Count - 1 || limit[limit.Length - 1] >= len)
27 return true;
28 int col = 0; // 処理中のコラム(ゼロから)
29 int start = 0; // 分割する文字列の先頭(ゼロから)
30 for (int cnt = Count - 1; col < cnt; ++col) // 最後のコラムだけ特別な処理が必要
31 {
32 row[(col + 1).ToString()] = val.Substring(start, limit[col] - start).Trim();
33 start = limit[col];
34 }
35 row[(col + 1).ToString()] = val.Substring(start);
36 return true;
37 }
38 protected virtual int[] GetLimits() => null;
39 }
6~8行目 | 1コラムしかない特殊な CSV ファイルとしている部分です |
14~18行目 | 読み込み終了後に、複数のコラムに分割しています |
38行目 | GetLimits() メソッドを派生クラスで再定義してください |
実は、この複数コラムへの分割のやり方では問題があり得ます。この分割方法は、文字数を基準にして分割しています。もし、固定長テキストファイルを作成したシステムが文字のバイト数を基準にしており、
文字のバイト数と文字数とが異なる場合があったとすると問題が発生します。それはどのような場合かといいますと、「半角カタカナ」文字があった場合です。半角カタカナの濁音や促音-例えば「ガ」-は2バイトで構成されますが、
#C で使用している文字コード UTF-16 では1文字となります。固定長テキストファイルに、会社名のフリガナが半角カタカナで含まれている場合には、まずこの問題に遭遇するでしょう。XX会社はきっとあります。
そのような場合は、派生したクラスで DivideColumns() メソッドをバイト数で分割するように再定義してください。もちろん、GetLimits() メソッドはバイト数を基準にした配列を返す必要があります。
書き出し用のインターフェース IWritable をデフォルト実装したクラスです。書き出し用のクラスが必要な場合は、このクラスから派生して利用してください。
読み込み用クラスと違って、変換処理と書き出し処理を行うため、やや複雑な構造となっています。特に複雑になっているのは変換処理の部分です。
変換処理は、原理的には任意の項目を自由に変換できるメソッド TransferValiable() があれば記述できます。実際にこのメソッドだけを使用してプログラムを記述したところ、メソッドが極めて長くなってしまいました。
そこで、ある特定な条件の場合だけを対象とした Transfer() メソッドと、自由な記述が可能な TransferValiable() の2つのメソッドに分割しました。さらに、 Transfer() メソッドでは次の4パターンに処理を分けています。
このようにすることで、 TransferValiable() を使用するのは、変換に何らかのプログラムが介在する必要がある場合に限定することができます。
1 public class Writer : IWritable
2 {
3 public virtual String GetName() => Name;
4 public void SetName(String name) => Name = name;
5 public virtual String GetError() => Result;
6 public virtual bool SetParameters(Object[] args) => true;
/// <summary>
/// 読み込んだレコードにフィルターを適応して、出力したいレコードを限定するためのラムダ式
/// </summary>
7 public Func<DataRow, bool> Filter
8 {
9 get; set;
10 }
/// <summary>
/// 書出し用データテーブルのコラム数
/// </summary>
11 public int Count
12 {
13 get; set;
14 }
15 public String Name
16 {
17 get; set;
18 }
19 public String Result
20 {
21 get; protected set;
22 }
23 public Writer()
24 {
25 Filter = null;
26 }
27 public virtual void Clear()
28 {
29 Result = String.Empty;
30 }
/// <summary>
/// 書込み用前処理と書込み処理のラムダ式を返す
/// デフォルトでは、それぞれ DefaultPreWrite() と DefaultWrite() を返す
/// 必要に応じて PreWriteLambda(), DefaultPreWrite(), WriteLambda(), DefaultWrite() を再定義すること
/// </summary>
31 public virtual Func<DataTable, DataTable, bool> PreWriteLambda() => DefaultPreWrite;
32 public virtual Func<DataTable, bool> WriteLambda() =< DefaultWrite;
/// <summary>
/// デフォルトで用意されている書出し前処理
/// </summary>
/// <param name="from">読み取ったデータを格納したデータテーブル</param>
/// <param name="to">書出し用のデータテーブル</param>
33 protected virtual bool DefaultPreWrite(DataTable from, DataTable to) => true;
/// <summary>
/// デフォルトで用意されている書込み処理
/// </summary>
34 protected virtual bool DefaultWrite(DataTable table) => true;
/// <summary>
/// 書出し処理に先立って、読み込んだテーブルに操作を加える必要がある場合に、サブクラスで再定義する
/// デフォルトでは、フィルターが設定されている場合に、そのフィルターに該当するレコードだけを対象にする
/// </summary>
35 protected virtual bool ChangeTable(DataTable table)
36 {
37 if (Filter == null)
38 return true;
39 table.AcceptChanges();
40 foreach (DataRow row in table.Rows)
41 {
42 if (! Filter(row))
43 {
44 row.Delete();
45 }
46 }
47 table.AcceptChanges();
48 return true;
49 }
/// <summary>
/// 読込み結果を書出し用データテーブルに反映する
/// </summary>
/// <param name="from">読み込み用のデータテーブル</param>
/// <param name="to">書き出し用のデータテーブル</param>
50 protected virtual bool Transfer(DataTable from, DataTable to)
51 {
// すべての行の転送を行う
52 int at = 0;
53 DataRow row;
53 foreach (DataRow s in from.Rows)
54 {
55 row = to.NewRow();
56 if (!Transfer(++at, s, row))
57 return false;
58 to.Rows.Add(row);
59 }
60 return true;
61 }
/// <summary>
/// 書出し用のデータテーブルの最終更新処理
/// 何もしないので、必要に応じてサブクラスで再定義する
/// </summary>
62 protected virtual bool TransferFinally(DataTable from, DataTable to) => true;
/// <summary>
/// from 行から to 行に値を転送するデフォルトの処理
/// 必要に応じて、Transfer(), GetZeroNumber(), GetOneNumber(), GetKeyValues(), GetValiablePair() を再定義すること
/// </summary>
/// <param name="at">行番号(1からの連番)</param>
63 protected virtual bool Transfer(int at, DataRow from, DataRow to)
64 {
65 try
66 {
// 列をゼロ埋めする
67 if (GetZeroNumberIndex() != null)
68 {
69 int[] nums = GetZeroNumberIndex();
70 foreach (var i in nums)
71 {
72 to[i] = "0";
73 }
74 }
75 else if (GetZeroNumberName() != null)
76 {
77 int[] nums = GetZeroNumberName();
78 foreach (var i in nums)
79 {
80 to[i.ToString()] = "0";
81 }
82 }
// 列を 1 埋めする
83 if (GetOneNumberIndex() != null)
84 {
85 int[] nums = GetOneNumberIndex();
86 foreach (var i in nums)
87 {
88 to[i] = "1";
89 }
90 }
91 else if (GetOneNumberName() != null)
92 {
93 int[] nums = GetOneNumberName();
94 foreach (var i in nums)
95 {
96 to[i.ToString()] = "1";
97 }
98 }
// 列を定数埋めする
99 if (GetKeyValuesIndex() != null)
100 {
101 Dictionary<int, String> dict = GetKeyValuesIndex();
102 foreach (var k in dict.Keys)
103 {
104 to[k] = dict[k];
105 }
106 }
107 else if (GetKeyValuesName() != null)
108 {
109 Dictionary<int, String> dict = GetKeyValuesName();
110 foreach (var k in dict.Keys)
111 {
112 to[k.ToString()] = dict[k];
113 }
114 }
// 読込み行から書出し行へ転送する
115 if (GetValiablePairIndex() != null)
116 {
117 Dictionary<int, int> pairs = GetValiablePairIndex();
118 int col;
119 foreach (var k in pairs.Keys)
120 {
121 col = pairs[k];
122 to[k] = from[col];
123 }
124 }
125 else if (GetValiablePairName() != null)
126 {
127 Dictionary<int, int> pairs = GetValiablePairName();
128 String fCol, tCol;
129 foreach (var k in pairs.Keys)
130 {
131 tCol = k.ToString();
132 fCol = pairs[k].ToString();
133 to[tCol] = from[fCol];
134 }
135 }
// プログラムの介在が必要な転送作業を行う
136 return TransferValiable(at, from, to);
137 }
138 catch (Exception ex)
139 {
140 Result = ex.Message;
141 return false;
142 }
143 }
/// <summary>
/// 行単位の転送作業で、プログラムの介在が必要な処理を行う
/// 必要に応じて、派生クラスで再定義する
/// </summary>
/// <param name="at">行番号(1から)</param>
/// <param name="from">転送元の行</param>
/// <param name="to">転送先の行</param>
145 protected virtual bool TransferValiable(int at, DataRow from, DataRow to) => true;
// 転送先の行で値をゼロに設定する必要のある列番号(0から)の配列を返す
146 protected virtual int[] GetZeroNumberIndex() => null;
// 転送先の行で値をゼロに設定する必要のある列名の配列を返す
147 protected virtual int[] GetZeroNumberName() => null;
// 転送先の行で値を 1 に設定する必要のある列番号(0から)の配列を返す
148 protected virtual int[] GetOneNumberIndex() => null;
// 転送先の行で値を 1 に設定する必要のある列名の配列を返す
149 protected virtual int[] GetOneNumberName() => null;
// 転送先の行で、定数に設定する必要のある列を、その列番号(0から)とその値のペアからなる辞書を返す
150 protected virtual Dictionary<int, String> GetKeyValuesIndex() => null;
// 転送先の行で、定数に設定する必要のある列を、その列名とその値のペアからなる辞書を返す
151 protected virtual Dictionary<int, String> GetKeyValuesName() => null;
// 転送元の行から転送先の行へ、そのまま値をコピーすればよい列番号(1から)のペアからなる辞書を返す
// キーは、転送先の列番号(0から)
// 値は、転送元の列番号(0から)
152 protected virtual Dictionary<int, int> GetValiablePairIndex() => null;
// 転送元の行から転送先の行へ、そのまま値をコピーすればよい列名のペアからなる辞書を返す
// キーは、転送先の列番号(0から)
// 値は、転送元の列番号(0から)
153 protected virtual Dictionary<int, int> GetValiablePairName() => null;
}
7~10行目 | 読みだしたデータの内、特定のレコードだけを処理対象としたい場合に、フィルターを設定することができます。処理対象としたいレコードの場合、 true を返すラムダ式を登録しておきます |
35~49行目 | 変換処理に先立って呼び出される処理です。デフォルトではフィルターが適応されます。必要に応じて再定義してください |
62行目 | すべての変換処理が終了した後で呼び出されます。必要に応じて再定義してください。全レコードに連番を付ける必要があったり、全レコードの合計値を計算する必要がある場合などで利用できます |
146~153行目 | 特定のパターンに該当する場合に呼び出されるメソッドです。必要に応じて再定義してください |
CSV ファイルに書き出すためのクラスです。 Writer クラスから派生しています。
直接 CSV ファイルを操作する処理は、CsvFile クラスを利用しています。このクラスについては、こちらを参照してください。
1 public class WriterCsv : Writer
2 {
3 protected CsvFile csvFile;
4 public WriterCsv()
5 {
6 csvFile = new CsvFile();
7 }
8 public override string GetError() => csvFile.Result;
/// <summary>
/// 再定義している、書出し前処理
/// </summary>
9 protected override bool DefaultPreWrite(DataTable from, DataTable to)
10 {
11 Clear();
// 読出し結果のデータテーブルに操作が必要な場合、派生クラスで再定義する
// デフォルトでは、true を返す
12 if (!ChangeTable(from))
13 return false;
// 書出し用 CsvFile の初期化
14 Initialize(csvFile);
// 書出し用のデータテーブルの構築
15 if (!Mapper.MakeTable(Count, to))
16 return false;
// 読込み結果を書出し用のデータテーブルに反映する
17 if (!Transfer(from, to))
18 return false;
// 書出しテーブルを最後に更新する処理
19 if (!TransferFinally(from, to))
20 return false;
21 return true;
22 }
/// <summary>
/// 再定義している、書出し処理
/// </summary>
23 protected override bool DefaultWrite(DataTable table)
24 {
25 bool flag = csvFile.Write(table);
26 if (!flag)
27 Result = csvFile.Result;
28 return flag;
29 }
/// <summary>
/// 書込み用 CsvFile の初期化処理
/// </summary>
30 protected virtual void Initialize(CsvFile csvFile)
31 {
32 csvFile.Path = Name;
33 csvFile.IsOverWrite = true;
34 }
35 }
固定長テキストファイルに書き出すためのクラスです。 WriterCsv クラスから派生しています。
このクラスが複雑になっているのは、コラムの文字長が指定された長さとピッタリ同じではない場合に、文字列を調整する必要があるためです。その調整は、次の値で決まってきます。
この調整方法が分かれば、プログラムの理解は難しくないと思います。
1 public class WriterConstant : WriterCsv
2 {
3 public enum DIRECTION { HEAD, TAIL }
4 public WriterConstant() { }
5 protected override void Initialize(CsvFile csvFile)
6 {
7 base.Initialize(csvFile);
8 csvFile.IsQuoted = false;
9 }
/// <summary>
/// 引数個分の空白文字列を返す
/// </summary>
/// <param name="num">空白の個数</param>
10 static public String ToSpace(int num)
11 {
12 return new string(' ', num);
13 }
/// <summary>
/// 引数個分の、指定文字からなる文字列を返す
/// </summary>
/// <param name="val">指定文字</param>
/// <param name="num">文字の個数</param>
14 static public String ToString(char val, int num)
15 {
16 return new string(val, num);
17 }
/// <summary>
/// 文字列をエンコーディングを使用して、適切長さに調整する
/// </summary>
/// <param name="val">調整したい文字列</param>
/// <param name="length">調整後の長さ(バイト長)</param>
/// <param name="encode">使用するエンコーディング</param>
/// <param name="dir">調整する位置は文字列の先頭/末尾の指定</param>
/// <param name="ch">長さが不足している場合の調整用の文字(ASCII文字であること)</param>
18 static public String ToPadding(String val, int length, Encoding encode, DIRECTION dir = DIRECTION.TAIL, char ch = ' ')
19 {
20 byte[] bytes = encode.GetBytes(val);
21 if (bytes.Length == length)
22 return val;
23 if (bytes.Length < length)
24 {
25 return toLonger(val, length, encode, dir, ch);
26 }
27 val = toShorter(val, length, encode, dir);
28 bytes = encode.GetBytes(val);
29 if (bytes.Length == length)
30 return val;
31 else if (bytes.Length > length)
32 return val;
33 return toLonger(val, length, encode, dir, ch);
34 }
/// <summary>
/// 数字を適切長さに調整する
/// </summary>
/// <param name="val">浮動小数点の文字列(整数部のみ有効)</param>
/// <param name="number">調整する文字列の長さ。文字列が長さを超える場合は、下の桁を削る</param>
35 static public String ToNumber(String val, int number)
36 {
37 float fvalue;
38 try
39 {
40 fvalue = float.Parse(val);
41 String num = ((int)fvalue).ToString();
42 return num.Length <= number ? num : num.Substring(0, number);
43 }
44 catch (Exception) { }
45 return "0";
46 }
/// <summary>
/// 数字を適切長さに調整する
/// </summary>
/// <param name="val">浮動小数点の文字列(整数部のみ有効)</param>
/// <param name="number">調整する文字列の長さ。文字列が長さを超える場合は、下の桁を削る</param>
/// <param name="dir">長さが短い場合に、調整する位置は文字列の先頭/末尾の指定</param>
/// <param name="ch">長さが短い場合に、調整に使用する文字(ASCII文字であること)</param>
47 static public String ToNumber(String val, int number, DIRECTION dir, char ch)
48 {
49 String txt = ToNumber(val, number);
50 if (txt.Length >= number)
51 return txt;
52 String tmp = ToString(ch, number - txt.Length);
53 return dir == DIRECTION.HEAD ? tmp + txt : txt + tmp;
54 }
/// <summary>
/// 浮動小数点数を適切長さに調整する
/// </summary>
/// <param name="val">浮動小数点数の文字列</param>
/// <param name="number">整数部の桁数</param>
/// <param name="fraction">小数点以下の桁数</param>
55 static public String ToNumber(String val, int number, int fraction)
56 {
57 int ival, fval;
58 float fvalue;
59 try
60 {
61 fvalue = float.Parse(val);
62 ival = (int)fvalue;
63 fval = (int)((fvalue - (float)ival) * 10 * fraction);
64 String num = ival.ToString();
65 if (num.Length > number)
66 num = num.Substring(0, number);
67 return num + "." + fval.ToString(ToString('0', fraction));
68 }
69 catch (Exception) { }
70 return ToString('0', number) + "." + ToString('0', fraction);
71 }
/// <summary>
/// 浮動小数点数を適切長さに調整する
/// </summary>
/// <param name="val">浮動小数点数の文字列</param>
/// <param name="number">整数部の桁数</param>
/// <param name="fraction">小数点以下の桁数</param>
/// <param name="dir">長さが短い場合に、調整する位置は文字列の先頭/末尾の指定</param>
/// <param name="ch">長さが短い場合に、調整に使用する文字(ASCII文字であること)</param>
72 static public String ToNumber(String val, int number, int fraction, DIRECTION dir, char ch)
73 {
74 String txt = ToNumber(val, number, fraction);
75 if (txt.Length >= number + fraction + 1)
76 return txt;
77 String tmp = ToString(ch, number + fraction + 1 - txt.Length);
78 return dir == DIRECTION.HEAD ? tmp + txt : txt + tmp;
79 }
80 static private String toLonger(String val, int length, Encoding encode, DIRECTION dir, char ch)
81 {
82 byte[] bytes = encode.GetBytes(val);
83 String txt = ToString(ch, length - bytes.Length);
84 return dir == DIRECTION.HEAD ? txt + val : val + txt;
85 }
86 static private String toShorter(String val, int length, Encoding encode, DIRECTION dir)
87 {
88 byte[] bytes;
89 String tmp = val;
90 while (true)
91 {
92 if (tmp.Length == 1)
93 break;
94 bytes = encode.GetBytes(tmp);
95 if (bytes.Length <= length)
96 break;
97 tmp = dir == DIRECTION.HEAD ? tmp.Substring(1) : tmp.Substring(0, tmp.Length - 1);
98 }
99 return tmp;
100 }
101 }
Reader クラスを定義しましょう。以下のプログラム片を参考にしてください。
1 public class MyReader : ReaderCsv
2 {
3 public MyReader() { }
4 protected override void Initialize(CsvFile csvFile)
5 {
6 base.Initialize(csvFile);
7 csvFile.NeedHeader = false;
8 }
9 }
以下は、簡単な説明です。
1行目 | クラス名を MyReader とし、 ReaderCsv クラスから派生します |
4~9行目 | 初期化処理です。CSV ファイルには見出しはないものとします |
Writer クラスを定義します。以下のプログラム片を参考にしてください。
1 public class MyWriter : WriterCsv
2 {
3 public MyWriter() { }
4 protected override int[] GetZeroNumber() => zero;
5 int[] zero = { 6, 23, 24, 84, 87 };
6 protected override int[] GetOneNumber() => one;
7 int[] one = { 1, 3, 5, 9, 12, 17, 25, 99 };
8 protected override Dictionary<int, String> GetKeyValues() => dict;
9 Dictionary<int, String> dict = new Dictionary<int, String>()
10 {
11 { 8, "0110" },
12 { 75, "JPY" }
13 };
14 protected override Dictionary<int, int> GetValiablePair() => pairDict;
15 Dictionary<int, int> pairDict = new Dictionary<int, int>() {
16 { 2, 3 },
17 { 229, 7 },
18 };
19 protected override bool TransferValiable(int at, DataRow from, DataRow to)
20 {
21 try
22 {
23 String val;
24 val = (String)from["4"];
25 DateTime? tmp = getDay(val);
26 if (tmp != null)
27 {
28 to["36"] = ((DateTime)tmp).ToString("yyyy/MM/dd");
29 }
30 }
31 catch (Exception ex)
32 {
33 Result = ex.Message;
34 return false;
35 }
36 }
1行目 | クラス名を MyWriter とし、 WriterCsv クラスから派生します |
4行目 | 0を設定するコラムを返します |
6行目 | 1を設定するコラムを返します |
8行目 | 定数を設定するコラムを返します |
14行目 | 読みこんだ値をそのまま書き出しに設定するコラムを返します |
19~36行目 | 変換にはプログラムが必要な部分を記述します |
Mapper クラスを定義します。以下のプログラム片を参考にしてください。
1 public class MyMapper : Mapper
2 {
3 public MyMapper() : this(new MyReader(), new MyWriter()) { }
4 public MyMapper(Reader reader, Writer writer) : base(reader, writer)
5 {
6 reader.Count = 30;
7 writer.Count = 401;
8 }
9 }
1行目 | クラス名を MyMapper とします |
3行目 | Reader には MyReader を使用し、Writer には MyWriter を使用します |
6行目 | 読みだしのコラム数は30です |
7行目 | 書き込みのコラム数は401です |
以下のプログラム片を参考にして、処理を開始してください
1 IReadable reader = new MyReader();
2 reader.SetName("input.csv");
3 IWritable writer = new MyWriter();
4 writer.SetName("putput.csv");
5 Mapper mapper = new MyMapper(reader, writer);
6 Converter obj = new Converter(mapper);
7 if (!obj.Run())
8 {
// エラー処理
9 }
読みこんだデータをすべて処理するのではなく、ある条件によってフィルターをかけたい場合は、 Writer クラスのフィルタープロパティを使うことができます。
1 public MyWriter()
2 {
3 Filter = (row) =>
4 {
5 string val = row["6"] == DBNull.Value ? "" : (String)row["6"];
6 return val.Length > 0;
7 };
8 }
1行目 | MyWriter クラスのコンストラクタでフィルターを設定しています |
3~7行目 | 読みこんだレコードのコラム名「6」の値が、空文字列でないレコードを処理対処とします。処理の対象としない場合は false を返してください |
Writer クラスの ChangeTable() メソッドをオーバーライドすることでも対処することができます。標準の ChangeTtable() メソッドでは、すべてのレコードに対してフィルターを適応しているだけです。
そのため、フィルターを利用することを最初に検討してください。
ある変換処理を実施し、その結果をさらに後段の変換処理に連結することができます。このためには、 Converter クラスの Connect() メソッドを利用できます。
こちらを参考にしてください。