うぃろぅ.log

140字で綴りきれない日々の徒然備忘録

【C#】 CSVをDictionaryのListとして読み込む with 黒魔術

うぃろぅです。

vviilloovv.hatenablog.com

前回の記事の関連です。
そもそもDataTableなんて使わずともDictionary型のListにしてしまえばいいのではないか、と考えたので書いておきます。

で、書いてたらなんか楽しくなってきたので黒魔術コードが生成されました。
せっかくなので残しておこうという趣旨。

何がしたいか

ヘッダーつきのCSVファイルを読み込んでListとして返却したい

いよいよCsvHelperに頼ればいいって?私もそう思う。

車輪の再発明から得られる知見もあるはず。というかただコード書きたいだけ。

まずは素直に書いてみる

今回もTextFieldParserを利用していく。
CSVの先頭行にヘッダーがあるとする。

以下ソース。using System.ほげほげはよしなに書いてください…。

private static List<Dictionary<string, string>> CsvToList(string path)
{
  // お約束
  var parser = new TextFieldParser(path, Encoding.GetEncoding("Shift-JIS"));
  {
    TextFieldType = FieldType.Delimited,
    Delimiters = new string[] { "," }
  };

  // 読み込み
  var rows = new List<string[]>();
  while (!parser.EndOfData)
  {
    rows.Add(parser.ReadFields());
  }

  // 列名設定
  var header = new List<string>();
  foreach(var head in rows.First())
  {
    header.Add(head);
  }

  // 行追加
  var dicList = new List<Dictionary<string, string>>();
  foreach(var row in rows.Skip(1))
  {
    var dic = new Dictionary<string, string>();
    foreach(var i in Enumerable.Range(0, row.Length))
    {
      dic.add(header[i], row[i]);
    }
    dicList.Add(dic);
  }

  return dicList;
}

こんな感じ? 若干面倒に見えるのは

foreach(var i in Enumerable.Range(0, row.Length))
{
  dic.Add(header[i], row[i]);
}

の箇所だろうか。rowsのループ回数は行数なので、行数分のDictionaryを生成しているのだが、例えば

name,age,address
alice,17,jp
bob,20,us
charlie,19,uk

こんなCSVがあったとして、

ヘッダー(header) 要素(row) 添え字[i]
name alice 0
age 17 1
address jp 2
ヘッダー(header) 要素(row) 添え字[i]
name bob 0
age 20 1
address us 2

イメージ的にはこう順番に追加している。

2要素を同時に回す

dicの名前がセンスない。できるだけ脳のワーキングメモリは減らしたい。

添え字を使って回さない方法を考えたい。
添え字使うのってなんかもっさりしてるイメージない??ないか。そうか。

Zipメソッドを使えば対処できそう。なのでやってみる。

var dicList = new List<Dictionary<string, string>>();
foreach(var row in rows.Skip(1))
{
  dicList.Add(header.Zip(row, (head, data) =>
    new KeyValuePair<string, string>(head, data))
    .ToDictionary(keyVal => keyVal.Key, keyVal => keyVal.Value));
}

こんな感じ?
値がnullになる可能性がある場合はkeyVal.Value ?? ""とかでいけるんじゃないかな。

(1つめの要素).Zip((一緒に回したい要素), (一緒に回した値に対して行いたい処理))が書き方。
今回はDictionary型に変換したかったためKeyValuePairを使った。

もっといいやり方がありそうな気配しかしないので知っている方はコメントお願いします…。

もう少し短くしてみる

可読性的にはさっきのがギリギリな気がする。

でもdicListって名前がイケてない。それっぽい名前をつければそれでいいと思います

return rows
  .Skip(1)
  .Aggregate(new List<Dictionary<string, string>>(), (list, row) =>
  {
    list.Add(header.Zip(row, (head, item) =>
      new KeyValuePair<string, string>(head, item))
      .ToDictionary(kv => kv.Key, kv => kv.Value ?? ""));
    return list;
  });

これでいけるはず!!なんと1行。

  1. ヘッダーと要素をKeyValuePairとして1要素に集約
  2. KeyValuePairDictionaryに変換
  3. 変換したDictionaryListに追加
  4. 行数分(ヘッダー除く)畳み込み演算
  5. 演算した結果を返却

の順で処理が走っている。

試しに改行しないで書いてみると

return rows.Skip(1).Aggregate(new List<Dictionary<string, string>>(), (list, row) =>{list.Add(header.Zip(row, (head, item) =>new KeyValuePair<string, string>(head, item)).ToDictionary(kv => kv.Key, kv => kv.Value ?? ""));return list;});

こうなる。空白入れて236文字。Cobol的にはエラー3回分くらい。へへ。

ここまで書いてAggregate使う必要ないのではということに気がついた。

改良案。

return rows
  .Skip(1)
  .Select(row =>
    new Dictionary<string, string>(
      header
        .Zip(row, (head, item) =>
          new KeyValuePair<string, string>(head, item))
        .ToDictionary(kv => kv.Key, kv => kv.Value ?? ""))
  .ToList();

これだ(?)。

return rows.Skip(1).Select(row => new Dictionary<string, string>(header.Zip(row, (head, item) => new KeyValuePair<string, string>(head, item)).ToDictionary(kv => kv.Key, kv => kv.Value ?? "")).ToList();

空白入れて202文字。圧倒的エコ。

ちなみにデバッガー動かして確認したらちゃんと同じ中身になっていた。逆になぜうまくいってしまったのか。

†黒魔術完成†

順番に読んでいけば難しい要素なんてほとんどないのに書いていくうちに膨れ上がる。

こうして黒魔術は世に解き放たれていくのであった。

まあこれくらいのコードならよくあることでしょう。別にレビューしてもらうわけでもなし、楽しんだもん勝ちということでひとつ。

ではまた。