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>にして扱う、なんてこともできる。用意しておいて損はない関数だと思います。


コメント