Linqで1行ずつ与えられた、データを縦に分割しよう!(ToLookup使用)


目的

1行ずつ与えられるデータ(ファイルとか、標準入力とか)が重なって2次元配列みたいなデータを列で分割して、楽に変換したい。

サンプル

例えば、こんなデータがあったとします。

Sample.cs
var str = new List<string>()
              {
                  "  1,  2,  3,  4,  5,  6,  7,  8,  9, 10",
                  "  2,  4,  6,  8, 10, 12, 14, 16, 18, 20",
                  "  3,  6,  9, 12, 15, 18, 21, 24, 27, 30",
                  "  4,  8, 12, 16, 20, 24, 28, 32, 36, 40",
                  "  5, 10, 15, 20, 25, 30, 35, 40, 45, 50",
                  "  6, 12, 18, 24, 30, 36, 42, 48, 54, 60",
                  "  7, 14, 21, 28, 35, 42, 49, 56, 63, 70",
                  "  8, 16, 24, 32, 40, 48, 56, 64, 72, 80",
                  "  9, 18, 27, 36, 45, 54, 63, 72, 81, 90",
                  " 10, 20, 30, 40, 50, 60, 70, 80, 90,100",
              };

これを列(1列目だと、1,2,3, ... , 10 のように)ごとに取得したい。

for文で

forなら簡単に取得ができる

ForSample.cs
            var sample = new List<string>();
            for (int i = 0; i < str.Count; i++)
            {
                sample.Add(str[i].Split(',')[0]);
            }

            Console.WriteLine("forサンプル");
            Console.WriteLine(string.Join(",", sample));

こんな感じでこれを実行すると、こんな感じになる

1列目だけでいいのなら、このサンプルコードでもいいのだけど、複数行対応したいときにはネストをしないといけなくなる。
例えばこんな感じ

AllFor.cs
//// 1つ目のデータで配列の数を設定
            var length = str[0].Split(',').Length;

            // 各列のリストを初期化
            var sample = new List<List<string>>();
            for (int i = 0; i < length; i++)
            {
                sample.Add(new List<string>());
            }

            // 1行をカンマで分割し、それぞれのリストに追加していく
            for (int i = 0; i < str.Count; i++)
            {
                var oneLine = str[i].Split(',');
                for (int j = 0; j < length; j++)
                {
                    sample[j].Add(oneLine[j]);
                }
            }

            // 表示
            for (int i = 0; i < length; i++)
            {
                Console.WriteLine($"i : {i}");
                Console.WriteLine(string.Join(",", sample[i]));
            }

冗長な部分もあると思いますが、素直に書いたら大体こんな感じになると思います。意外と長くなってしまいます。

Linqで

次はLinqで書いてみたいと思います。こちらの方がやはり少ない行数で書けるということで、いいですね。慣れるまでは大変ですが。

LinqSample.cs
            // 1行をカンマで分割して、0からインデックスを追加
            var strWithIndex = str.Select(
                x => x.Split(',').Where(word => !string.IsNullOrWhiteSpace(word)).Select((word, i) => new { word, i }));
            // 全ての行を平坦化して、先ほど追加したインデックスでグループ化する.
            var oneColumnData = strWithIndex.SelectMany(word => word).ToLookup(x => x.i);

一応この2行で、列で取得することが可能です。
実際に取得するとこんな感じです。

LinqOutput.cs
            foreach (var oneColumn in oneColumnData)
            {
                Console.WriteLine("キー:" + oneColumn.Key);
                Console.WriteLine(string.Join(",", oneColumn.Select(x => x.word)));
            }

これで、2次元配列とかも簡単に列で取得することができますね!

簡単な解説

このままで終わるのもあれなので、一応解説をしておきます。

まず、LinqSample.csの1行目はコメントにも書いてある通り、1行をまずカンマで分割し、そのあと分割後の要素が{空、Null、string.Empty}ではない要素のみ取得し、その要素にインデックスを付与しています。
1つだけ実行するとこんな感じ

ここでの、iの値がそのまま列のインデックスとなります。

2つ目は、全部を列挙できるような状態に直してからインデックスでグループを作っていきます。

全部列挙(SelectMany(x => x))後の状態

ToLookupでグループ化した状態は先ほど見せた画像と同じです。

終わりに

いかがだったでしょうか?
次があれば、自分が便利だと思った、C#の標準メソッドなどを紹介したいです。
それではよいC#ライフを!