入力-変換-出力 のモデル  2020年7月29日記

0.初めに

初めに

 一見して良く分からないタイトルを付けましたが、もともとの問題は、次のものでした。

 CSV ファイルを読み込み、各行ごとに何らかの処理を行い、 CSV ファイルを出力したい。

  このようなプログラムを作成する必要がありました。もちろん、この問題に直接答えるプログラムを作成することは難しくありません。しかし、可能な限り汎用となるモデルを作っておきたいと考えました。

 そこで次のような条件を想定しました。

  • 入力の方法は、CSV ファイルに限定しない、幅広いものに対応できること
  • 行単位の変換処理には、特別な制限はもうけないこと
  • 出力の方法は、CSV ファイルに限定しない、幅広いものに対応できること

 以下では複数のインターフェース、複数のクラスが紹介されています。さて、この問題をどのように解決したでしょうか?

入力-変換-出力 のモデル図

 この章で紹介しているプログラムのモデル図を最初に示します。


 図にあります Converter は、以下で登場してくるクラスの名前です。このプログラムで中心的な役割を果たすことになります。図中の2つの円筒形のものは、データの格納場所を表します。左の円筒形は読みこんだデータの格納用に使用し、 右の円筒形はデータの書き出し用に使用します。このデータの格納には、標準クラスの DataTable クラスを使用します。さて、中心となる Converter クラスは、次の3つの処理が行われる場所を提供します。

  • 入力①
    何らかの方法で、入力データを読み込みます
  • 変換②
    読みこんだデータを1行単位に処理して、書き出し用に変換します
  • 出力③
    書き出し用に作成されたデータを出力します

 あとは、このモデルをどのようにプログラムで実現するかがテーマになります。 Converter クラスはこの処理を提供する場となりますが、処理自体にはかかわらないようにする必要があります。

1.解決方法


 ここでは、2つのインターフェースといくつかのクラスを紹介します。

 クラスはやや複雑ですから、ここでリストにしておきたいと思います。

  • Converter
    処理の中心となるクラスです。このクラスから派生クラスを作成する必要はないと思います
  • Mapper
    読み込み用クラスと書き出し用クラスのペアを管理するためのクラスです。必ず派生クラスを作成することになります
  • Reader
    読み込み用のインターフェースを実装したクラスです。必ず派生クラスを作成することになります
  • ReaderCsv
    CSV ファイルから読み込むためのクラスです。 Reader クラスから派生しており、さらにこのクラスから派生して利用します
  • ReaderConstant
    固定長のテキストファイルから読み込むためのクラスです。ReaderCsv クラスから派生しており、さらにこのクラスから派生して利用します
  • ReaderExcel
    Excel ファイルから読み込むためのクラスです。Reader クラスから派生しており、さらにこのクラスから派生して利用します。説明は省きます
  • Writer
    書き出し用のインターフェースを実装したクラスです。必ず派生クラスを作成することになります
  • WriterCsv
    CSV ファイルに書き出すためのクラスです。Writer クラスから派生しており、さらにこのクラスから派生して利用します
  • WriterConstant
    固定長のテキストファイルに書き出すためのクラスです。WriterCsv クラスから派生しており、さらにこのクラスから派生して利用します
  • WriterExcel
    Excel ファイルに書き出すためのクラスです。Writer クラスから派生しており、さらにこのクラスから派生して利用します。説明は省きます

 一覧にあるように、読み込みと書き出しのために複数のクラスが用意されています。このクラスは自由に組み合わせて利用することができます。例えば、Excel ファイルから読み込み、 CSV ファイルに書き出すといったことが可能です。 一覧には載せていませんが、データベースから直接読み込むためのクラス、PDF ファイルから読み込むためのクラス、さらにはデータベースに直接書き込むためのクラスも用意してあります。もちろん、自由に組み合わせることができるようになっています。

インターフェース

IReadable インターフェース

 モデル図にある、入力① に対応したインターフェースが 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 インターフェース

 モデル図にある、変換②と出力③ に対応したインターフェースが 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 クラス

 このモデルの中心となるクラスが 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() メソッドを呼び出してはいけません

Mapper クラス

 読み込みに使用するオブジェクトと書き出しに使用するオブジェクトのペアを管理するためのクラスです。実は、このクラスは「入力-変換-出力」の処理だけからすると、不要にすることができます。
 このクラスを導入した理由は、「入力-変換-出力」処理を行うオブジェクトをリフレクションを使用して、動的に自由に生成したいためです。 読み込み用と書き出し用のペアに関する情報を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からの連番となります

Reader クラス

 読み込み用のインターフェース 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    }
					

ReaderCsv クラス

 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    }
					

ReaderConstant クラス

 固定長のテキストファイルから読み込むためのクラスです。 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() メソッドはバイト数を基準にした配列を返す必要があります。

Writer クラス

 書き出し用のインターフェース IWritable をデフォルト実装したクラスです。書き出し用のクラスが必要な場合は、このクラスから派生して利用してください。
 読み込み用クラスと違って、変換処理と書き出し処理を行うため、やや複雑な構造となっています。特に複雑になっているのは変換処理の部分です。 変換処理は、原理的には任意の項目を自由に変換できるメソッド TransferValiable() があれば記述できます。実際にこのメソッドだけを使用してプログラムを記述したところ、メソッドが極めて長くなってしまいました。
 そこで、ある特定な条件の場合だけを対象とした Transfer() メソッドと、自由な記述が可能な TransferValiable() の2つのメソッドに分割しました。さらに、 Transfer() メソッドでは次の4パターンに処理を分けています。

  • 無条件に定数0を設定するコラム
    該当するコラムをコラム名で指定するか順番で指定することができます
  • 無条件い定数1を設定するコラム
    該当するコラムをコラム名で指定するか順番で指定することができます
  • 特定の定数を設定するコラム
    定数(文字列)とコラム名で指定するか、定数(文字列)をコラムの順番で指定することができます
  • 読みだしたコラムの値を書き出しのコラムに設定すればよい場合
    それぞれのコラムは、コラム名で指定するか順番で指定することができます

 このようにすることで、 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行目 特定のパターンに該当する場合に呼び出されるメソッドです。必要に応じて再定義してください

WriterCsv クラス

 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    }
					

WriterConstant クラス

 固定長テキストファイルに書き出すためのクラスです。 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   }
					

3.サンプル

一般的な開発手順

Reader クラスの定義

 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 クラスの定義

 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 クラスの定義

 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 クラスのフィルターを利用する方法

 読みこんだデータをすべて処理するのではなく、ある条件によってフィルターをかけたい場合は、 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 を返してください

ChangeTable() メソッドを利用する方法

 Writer クラスの ChangeTable() メソッドをオーバーライドすることでも対処することができます。標準の ChangeTtable() メソッドでは、すべてのレコードに対してフィルターを適応しているだけです。
 そのため、フィルターを利用することを最初に検討してください。

変換処理を連結する方法

 ある変換処理を実施し、その結果をさらに後段の変換処理に連結することができます。このためには、 Converter クラスの Connect() メソッドを利用できます。

 こちらを参考にしてください。