読者です 読者をやめる 読者になる 読者になる

C# + CsvHelperで、CSV中の日付っぽい文字列を C# のDateTime構造体にマッピングする

C# CSV

C#CSVファイルを読み書きする場合、CsvHelperが便利なのでNuGetからインストールして使っています。
JoshClose/CsvHelper: Library to help reading and writing CSV files

 
そんな中、

name,date1,date2,date3
hoge,20160101,20160102,20160103

のようなCSVファイルにて、日付っぽい値(yyyyMMdd)をC#のDateTime構造体にマッピングする必要があったので、メモを残します。

 

環境

  • Windows10
  • Visual Studio2015 Update1
  • .NET Framework 4.6.1
  • CsvHelper 2.13.5

 

実装の流れ

今回、

  • コンソールアプリとして作成
  • CSVファイルとC#オブジェクトは、手動でマッピングする

という形で進めます。

 

マッピング先のクラスを作成
public class CsvFile
{
    public string Name { get; set; }
    public DateTime Date1 { get; set; }
    public DateTime Date2 { get; set; }
    public DateTime Date3 { get; set; }
}

 

マッピング用のクラスを作成

CSVファイルと上記のCsvFileクラスをマッピングするためのクラスを作成します。

通常であれば、

public class Mapper : CsvHelper.Configuration.CsvClassMap<CsvFile>
{
    public Mapper()
    {
        Map(m => m.Name).Index(0);
        Map(m => m.Date1).Index(1);
        ...
    }
}

とすれば、

となります。

ただ、このまま実行すると、Date1へのマッピング文字列は有効な DateTime ではありませんでした。というエラーで動作しません。CSVファイルのデータが20160101と、日付っぽく見えて日付ではない値になっているためです。

そのため、いくつかの方法を使って、日付っぽい値とDateTime構造体のマッピングを行ってみます。

 

カスタムタイプコンバータを作成する方法

公式ドキュメントのWikiにある、カスタムタイプコンバータを使ってマッピングしてみます。
Custom TypeConverter · JoshClose/CsvHelper Wiki

WikiではDefaultTypeConverterを継承したクラスを作成・使用していますが、ソースコードを読んでみたところ、DefaultTypeConverterを継承したDateTimeConverterがありました。
CsvHelper/DateTimeConverter.cs at master · JoshClose/CsvHelper

そのため、今回はDateTimeConverterを継承したカスタムタイプコンバータを作成・使用します。

public class CsvDateConverter : CsvHelper.TypeConversion.DateTimeConverter
{
    public override object ConvertFromString(CsvHelper.TypeConversion.TypeConverterOptions options, string text)
    {
        if (text == null)
        {
            return base.ConvertFromString(options, null);
        }

        if (text.Trim().Length == 0)
        {
            return DateTime.MinValue;
        }
        return DateTime.ParseExact(text, "yyyyMMdd", null);
    }
}

 
作成したカスタムタイプコンバータは

Map(m => m.Date1).Index(1).TypeConverter<CsvDateConverter>();

のように使います。

 

ConvertUsing()を使う方法

カスタムタイプコンバータでは別途クラスを用意する必要があったため、今回のような単純な例で使うのは少々手間でした。

そこで、ConvertUsing()を使ったマッピングを試してみます。
CsvHelper - Convert Using

ConvertUsingのパラメータ(ここではrowという名前)に、CSVファイルの一行分のデータが入っています。そのため、GetField()メソッドCSVファイルの列インデックスを渡すことで、列を指定してマッピングできます。
CsvHelper - Reading individual fields

Map(m => m.Date2).ConvertUsing(row => DateTime.ParseExact(row.GetField<string>(2), "yyyyMMdd", null));

 

TypeConverterOption()を使う方法

ConvertUsing()を使っても少々手間なので、再度CsvHelper.TypeConversion.DateTimeConverterクラスを眺めてみます。

すると、ConvertFromString()メソッドの引数で受け取ったTypeConverterOptions.Formatを使い、DateTime構造体へと変換・マッピングしていました。
CsvHelper/DateTimeConverter.cs - JoshClose/CsvHelper

TypeConverterOptions.Formatはどこで設定するのかをたどっていくと、CsvHelper.Configuration.CsvPropertyMap.TypeConverterOption()メソッドがありました。TypeConverterOption()にはオーバーロードがありましたが、string型で渡すのが良さそうでした。
CsvHelper/CsvPropertyMap.cs at JoshClose/CsvHelper

 
以上より、TypeConverterOption()メソッドを使って、変換時に使うフォーマットを文字列で指定してマッピングします。
CsvHelper - Type Converter Options

Map(m => m.Date3).Index(3).TypeConverterOption("yyyyMMdd");

 

結果確認

こんな感じの

static void Main(string[] args)
{
    var runDir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
    var path = System.IO.Path.Combine(runDir, @"test.csv");

    using (var sr = new System.IO.StreamReader(path))
    using (var csv = new CsvHelper.CsvReader(sr))
    {
        csv.Configuration.RegisterClassMap<Mapper>();
        var records = csv.GetRecords<CsvFile>();

        foreach (var r in records)
        {
            Console.WriteLine($"date1: {r.Date1}\ndate2: {r.Date3}\ndate3: {r.Date3}");
        }
        Console.ReadKey();
    }
}

コンソールアプリを作成して実行したところ、

date1: 2016/01/01 0:00:00
date2: 2016/01/03 0:00:00
date3: 2016/01/03 0:00:00

のように表示され、いずれのパターンでもマッピングができていました。

 

ソースコード

GitHubに上げました。DateConverterSampleプロジェクトが今回のソースコードです。
thinkAmi-sandbox/CsvHelperSample

 

参考