[C#][.NET] CSVファイルの書き出し(列とプロパティをバインディング、属性を使って)

[CSVの読み込み](http://qiita.com/sengoku/items/578bc526f3c14f8625d9)ができるようになったら、書き込みもできないといけないよね。

作りました。

CSV化は、例によって「Perl正規表現雑技」からいただきました。
感謝 :smile: 

**2018/2/1 追記**
いくらか手直しして、こちらにソースを置きました。
https://github.com/sengokyu/csvreader-csvwriter-for-dot-net


# 概要

列と行がある一般的なCSVを書き出します。
列数は固定（行によって列数が変わらない）です。

## 特長

* CSV列とクラスのプロパティを属性でバインディングします。
* 値中にあるカンマに対応しています。
* 値中にある改行に対応しています。
* 値中にあるダブルクォートに対応しています。


# 使い方

## 準備

CSVとして書き出したいクラスのプロパティに属性`CSVColumn`を付与します。

```csharp:SampleBean.cs
    public class SampleBean
    {
        public string Ignored { get; set; }

        [CSVColumn(1)]
        public string Column1 { get; set; }

        [CSVColumn(2, Name = "Original, \"Name")]
        public string Column2 { get; set; }

        [CSVColumn(3)]
        public int MyNumber { get; set; }
    }
```

属性の引数は2つです。

| 引数 | 必須 | 説明 |
|:--|:--|:--|
| Order | 〇 | CSVとして書き出すときの列順を指定します。  |
| Name |   | CSVファイルのヘッダに出力する名前を指定します。未指定の時はプロパティ名から自動生成します。  |

## 書き出し

あらかじめ`Stream`を用意しておきます。
CSVに出力するクラスをジェネリクスで渡します。

あとは`WriteHeaderLine`を呼べばヘッダが書き出され、`WriteLine`を呼べば行が書き出されます。

```csharp:
IEnumebable<SampleBean> sampleBeans;
// 出力したいクラス群があるとするじゃろ。

// ...

using (var writer = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
{
    writer.WriteHeaderLine(); // ヘッダ行を書き出します。

    foreach (var i in sampleBeans) {
        writer.WriteLine(i); // 書き出し
    }

    writer.Close();
}
```

# ソース

## 属性

```csharp:CSVColumnAttribute.cs
    /// <summary>
    /// CSV column definition
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
    public class CSVColumnAttribute : System.Attribute
    {
        private readonly int _order;

        public CSVColumnAttribute(int order)
        {
            _order = order;
        }

        public int Order { get { return _order; } }
        public string Name { get; set; }
    }
```

## インターフェース

```csharp:ICSVWriter.cs
    /// <summary>
    /// Write object to the stream as CSV
    /// </summary>
    public interface ICSVWriter<T> : IDisposable
    {
        /// <summary>
        /// Write a header line
        /// </summary>
        void WriteHeaderLine();

        /// <summary>
        /// Write a line
        /// </summary>
        /// <param name="record"></param>
        void WriteLine(T record);
    }
```

## コンクリートクラス

```csharp:CSVWriter.cs
public class CSVWriter<T> : ICSVWriter<T>
    {
        private static readonly string DELIMITER = ",";
        private readonly StreamWriter _writer;
        private List<BindingProperty> _bindingProperties;

        public CSVWriter(Stream stream, Encoding encoding)
        {
            _writer = new StreamWriter(stream, encoding);
            _bindingProperties = extractBindingProperties();
        }

        private List<BindingProperty> extractBindingProperties()
        {
            var targetType = GetType().GetGenericArguments()[0];

            return targetType
                .GetProperties()
                .Select(i => new BindingProperty()
                {
                    Property = i,
                    CSVColumn = i.GetCustomAttributes(typeof(CSVColumnAttribute), false).FirstOrDefault() as CSVColumnAttribute
                })
                .Where(i => i.CSVColumn != null)
                .OrderBy(i => i.CSVColumn.Order)
                .ToList();
        }


        public void WriteLine(T record)
        {
            var values = _bindingProperties
                .Select(i => i.Property.GetValue(record))
                .Select(i => Quote(i))
                .ToArray();

            _writer.WriteLine(string.Join(DELIMITER, values));
        }

        public void WriteHeaderLine()
        {
            var headers = _bindingProperties
                            .Select(i => i.CSVColumn.Name ?? CamelCase2Title(i.Property.Name))
                            .Select(i => Quote(i))
                            .ToArray();

            _writer.WriteLine(string.Join(DELIMITER, headers));
        }

        public void Flush()
        {
            _writer.Flush();
        }

        public void Close()
        {
            _writer.Close();
        }

        private string CamelCase2Title(string src)
        {
            return Regex.Replace(src, "(?<!^)([A-Z])(?![A-Z])", " ${1}");
        }


        private string Quote(object src)
        {
            string ssrc = src != null ? src.ToString() : "";

            // via http://www.din.or.jp/~ohzaki/perl.htm#CSVfromValues
            // join ',', map {(s/"/""/g or /[\r\n,]/) ? qq("$_") : $_} @values;

            if (Regex.Match(ssrc, "[\"\\r\\n,]").Success)
            {
                return "\"" + ssrc.Replace("\"", "\"\"") + "\"";
            }
            else
            {
                return ssrc;
            }
        }

        private class BindingProperty
        {
            internal PropertyInfo Property;
            internal CSVColumnAttribute CSVColumn;
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _writer.Dispose();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~CSVWriter() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
        #endregion
    }
```

# テスト

```csharp:CSVWriterTests.cs
[TestClass]
    public class CSVWriterTests
    {
        [TestMethod]
        public void TestWriterHeaderLine()
        {
            using (var stream = new MemoryStream())
            using (var instance = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
            {
                instance.WriteHeaderLine();

                instance.Flush();

                stream.Seek(0, SeekOrigin.Begin);

                var result = Encoding.UTF8.GetString(stream.ToArray());

                Check.That(result).IsEqualTo("Column1,\"Original, \"\"Name\",My Number\r\n");
            }
        }

        [TestMethod]
        public void TestWriteLine()
        {
            var bean = new SampleBean()
            {
                Column1 ="value1",
                Column2 ="value\nvalue,value\"",
                MyNumber= 1234
            };

            using (var stream = new MemoryStream())
            using (var instance = new CSVWriter<SampleBean>(stream, Encoding.UTF8))
            {
                instance.WriteLine(bean);

                instance.Flush();

                stream.Seek(0, SeekOrigin.Begin);

                var result = Encoding.UTF8.GetString(stream.ToArray());

                Check.That(result).IsEqualTo("value1,\"value\nvalue,value\"\"\",1234\r\n");
            }

        }

        private class SampleBean
        {

            public string Ignored { get; set; }

            [CSVColumn(1)]
            public string Column1 { get; set; }

            [CSVColumn(2, Name = "Original, \"Name")]
            public string Column2 { get; set; }

            [CSVColumn(3)]
            public int MyNumber { get; set; }
        }

    }
```