【C#】DataTableをグループごとに分割しList<DataTable>に格納する【現場の備忘録】

プログラム

1つのDataTableに全データがまとまって入っているんだけど、これを「店舗ごと」「担当者ごと」みたいにグループで分けて処理したい、という場面が現場だと地味によくある。

1件ずつifで条件分岐して振り分けても書けなくはないけど、列や条件が変わるたびに書き直すのはしんどい。

なので任意のキー列でDataTableをグループ分割して、List<DataTable>に格納する汎用関数を用意しておく。

グループ分割処理

    /// <summary>
    /// DataTableを指定列の値ごとにグループ分割してList<DataTable>で返す
    /// </summary>
    /// <param name="dt">分割元のDataTable</param>
    /// <param name="columnName">グループのキーにする列名</param>
    /// <returns>グループごとのDataTableのList(キーの登場順)</returns>
    public static List<DataTable> SplitByColumn(DataTable dt, string columnName)
    {
        var result = new List<DataTable>();
        // 元テーブルがnull、または指定列が無い場合は空Listを返す
        if (dt == null || !dt.Columns.Contains(columnName)) return result;
 
        // グループのキー -> そのグループ用テーブル の対応表(登場順を保持)
        var map = new Dictionary<string, DataTable>();
 
        foreach (DataRow row in dt.Rows)
        {
            // キーの値(nullは空文字として扱う)
            string key = row[columnName]?.ToString() ?? string.Empty;
 
            // 初めて出てきたキーなら、元テーブルと同じ「列だけ」の空テーブルを用意
            if (!map.ContainsKey(key))
            {
                DataTable groupTable = dt.Clone();  // Cloneは列定義だけコピー(行はコピーしない)
                map.Add(key, groupTable);
                result.Add(groupTable);             // 登場順でListに追加
            }
 
            // キーに対応するテーブルへ、その行を取り込む
            map[key].ImportRow(row);
        }
 
        return result;
    }

やっていることはコメントの通り。ポイントはdt.Clone()で「列の定義だけ」をコピーした空テーブルを作り、そこへImportRowで行を移していくところ。

CopyToDataTable()を使う手もあるけど、対象行が0件だと例外になるので、空グループでも安全なClone+ImportRowの方が現場では扱いやすい。

キーの管理にDictionaryを使っているので、グループの並びは元データでの登場順のまま保たれる。

注意点は、指定した列名が存在しないと空のListが返ること。列名のスペルミスに気付きやすいよう、必要なら例外を投げる作りにしてもいい。

実装例

下記の例はVisualStudio2022.NET8.0、作成プロジェクトはコンソールアプリ、OSはWindows11で実装。

Program.csにそのままコピペで動く。「CreateTable()」で店舗入りのテーブルを作成して、「SplitByColumn(dt, “TENPO”)」で店舗ごとに分割している。

Program.cs
using System;
using System.Collections.Generic;
using System.Data;
 
class Program
{
    static void Main()
    {
        // データテーブルを用意
        DataTable dt = CreateTable();
 
        // 「TENPO(店舗)」列でグループごとに分割
        List<DataTable> tables = SplitByColumn(dt, "TENPO");
 
        // グループごとに中身を表示
        foreach (DataTable table in tables)
        {
            // 各グループの先頭行から店舗名を取得
            string tenpo = table.Rows[0]["TENPO"].ToString()!;
            Console.WriteLine($"■{tenpo}({table.Rows.Count}件)");
 
            foreach (DataRow dr in table.Rows)
            {
                Console.WriteLine($" {dr["SHOHIN"]} : {dr["KINGAKU"]}円");
            }
        }
    }
 
    /// <summary>
    /// 確認用のデータテーブルを作成
    /// </summary>
    static DataTable CreateTable()
    {
        DataTable dt = new DataTable();
 
        dt.Columns.Add("TENPO", typeof(string));   // 店舗
        dt.Columns.Add("SHOHIN", typeof(string));  // 商品
        dt.Columns.Add("KINGAKU", typeof(int));    // 金額
 
        dt.Rows.Add("東京", "りんご", 120);
        dt.Rows.Add("大阪", "みかん", 80);
        dt.Rows.Add("東京", "ぶどう", 300);
        dt.Rows.Add("名古屋", "なし", 150);
        dt.Rows.Add("大阪", "いちご", 400);
        dt.Rows.Add("東京", "もも", 250);
 
        return dt;
    }
 
    /// <summary>
    /// DataTableを指定列の値ごとにグループ分割してList<DataTable>で返す
    /// </summary>
    public static List<DataTable> SplitByColumn(DataTable dt, string columnName)
    {
        var result = new List<DataTable>();
        if (dt == null || !dt.Columns.Contains(columnName)) return result;
 
        var map = new Dictionary<string, DataTable>();
 
        foreach (DataRow row in dt.Rows)
        {
            string key = row[columnName]?.ToString() ?? string.Empty;
 
            if (!map.ContainsKey(key))
            {
                DataTable groupTable = dt.Clone();
                map.Add(key, groupTable);
                result.Add(groupTable);
            }
 
            map[key].ImportRow(row);
        }
 
        return result;
    }
}

実行するとこうなる。東京・大阪・名古屋のグループにちゃんと分かれて、件数もそれぞれ集計されている。

実行結果
■東京(3件)
 りんご : 120円
 ぶどう : 300円
 もも : 250円
■大阪(2件)
 みかん : 80円
 いちご : 400円
■名古屋(1件)
 なし : 150円

さいごに

グループごとに分割できると、そのままグループ単位でCSV出力したり、小計を出したりといった下処理がぐっと楽になる。

以前紹介したDataTable→List変換と組み合わせれば、分割した各DataTableをさらにList<自作Class>にして扱う、なんてこともできる。用意しておいて損はない関数だと思います。

コメント

タイトルとURLをコピーしました