C# 데이터테이블에서 중간 소계를 구하는 방법, 괜찮은 방법이 없을까요? 생각해보다가 만들어보았는데요, 다음 예시보다 좋은 방법이 있다면 공유 좀 해주세요~ 😘
데이터 분석이나 보고서를 작성할 때 카테고리별 또는 서브카테고리별 소계를 자동으로 계산해주는 기능이 있다면 작업의 효율을 크게 높일 수 있을 것 같아요.
특히 C#에서는 DataTable을 사용하여 데이터를 관리하는 경우가 많은데요, 소계 행을 동적으로 생성하여 DataTable에 추가하는 방법을 구현할 수 있습니다. 다음은 DataTable을 기반으로, 특정 조건에 따라 중간 소계를 구하고, 이를 테이블에 추가하는 방법을 다룬 예시 코드입니다.
예시 코드
다음 코드는 DataTable에 데이터 행을 추가하고, Category 및 Subcategory별로 소계를 계산해 추가하는 함수입니다.
void BeomSang()
{
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Category", typeof(string));// 키1 컬럼
dataTable.Columns.Add("Subcategory", typeof(string));// 키2 컬럼
dataTable.Columns.Add("Amount", typeof(decimal));// 소계를 계산할 컬럼
dataTable.Columns.Add("Tax", typeof(decimal));// 소계를 계산할 컬럼
dataTable.Columns.Add("Description", typeof(string));// 소계에 포함되지 않을 텍스트 컬럼
// 예제 데이터 추가
dataTable.Rows.Add("A", "a", 100, 10, "BEOMSANG");
dataTable.Rows.Add("A", "a", 200, 20, "BEOM");
dataTable.Rows.Add("A", "b", 500, 50, "SANG");
dataTable.Rows.Add("B", "b", 150, 15, "BS");
dataTable.Rows.Add("B", "b", 50, 5, "SB");
// 합계 행 추가
DataRow totalRow = dataTable.NewRow();
totalRow["Category"] = "합계";
totalRow["Subcategory"] = "합계";
foreach (DataColumn column in dataTable.Columns)
{
switch (column.ColumnName)
{
case "Category":
case "Subcategory":
break;
default:
if (column.DataType == typeof(decimal))
{
totalRow[column.ColumnName] = dataTable.Compute($"SUM({column.ColumnName})", string.Empty);
}
break;
}
}
dataTable.Rows.Add(totalRow);
// 그룹별 소계 계산 변수
var groups = dataTable.AsEnumerable()
.Where(x => x["Category"].ToString() != "합계" && x["Subcategory"].ToString() != "합계")
.GroupBy(x => new { key1 = x["Category"].ToString(), key2 = x["Subcategory"].ToString() })
.Select(x => new { KeyCol = x.Key });
// 소계 행 추가
foreach (var group in groups)
{
DataRow subTotalRow = dataTable.NewRow();
subTotalRow["Category"] = $"{group.KeyCol.key1} 소계";
subTotalRow["Subcategory"] = $"{group.KeyCol.key2 } 소계";
for (int c = 0; c < dataTable.Columns.Count; c++)
{
switch (dataTable.Columns[c].ColumnName)
{
case "Category":
case "Subcategory":
break;
default:
if (dataTable.Columns[c].DataType == typeof(decimal))
{
subTotalRow[dataTable.Columns[c]] = dataTable.Compute($"SUM({dataTable.Columns[c].ColumnName})", $"Category = '{group.KeyCol.key1}' AND Subcategory = '{group.KeyCol.key2}'");
}
break;
}
}
dataTable.Rows.Add(subTotalRow);
}
// 결과 데이터 정렬
dataTable = dataTable.AsEnumerable().OrderBy(x => x["Category"].ToString() == "합계" ? 1 : 0)
.ThenBy(x => x["Category"].ToString().Replace(" 소계", ""))
.ThenBy(x => x["Subcategory"].ToString().Replace(" 소계", ""))
.ThenBy(x => x["Category"].ToString().EndsWith("소계") ? 1 : 0)
.ThenBy(x => x["Subcategory"].ToString().EndsWith("소계") ? 1 : 0)
.CopyToDataTable();
}
코드 분석
데이터 생성 및 컬럼 정의
첫 번째로,DataTable객체를 만들고Category,Subcategory,Amount,Tax,Description컬럼을 추가합니다. 각각의 컬럼에 데이터 유형을 설정하고, 예제 데이터를 추가할게요.합계 행 추가
합계를 구하여 행을 추가합니다.Compute메서드를 사용할 텐데요, 아래에서 소계를 구할 때에도 다시 활용할 예정입니다~그룹별 소계 계산 및 추가
GroupBy메서드를 사용해Category와Subcategory별로 데이터를 그룹화하고,Compute메서드를 통해Amount와Tax컬럼의 소계를 계산합니다. 각각의 소계 행은Category와Subcategory이름에 '소계'라는 텍스트가 추가된 상태로DataTable에 추가해 볼게요.결과 정렬
마지막으로,OrderBy와ThenBy를 이용해Category와Subcategory값이 '소계'와 '합계'인 행이 지정된 순서대로 위치하도록 정렬합니다.
요약 및 결론
데이터 분석이나 보고서에서 각 그룹별 데이터의 합계를 한눈에 파악할 수 있도록 돕고자 해보았습니다~ 특히, DataTable.Compute 메서드를 활용해 간단하게 합계를 구할 수 있으므로, C#을 활용해 데이터 테이블을 구성하고 계산하는 과정에서 유용하게 활용할 수 있을 것 같아요. 참고하여 사용하면서 더 좋은 코드가 있다면 공유 좀 해주세요~
메서드 예제
메서드를 간단하게 만들어보았습니다. 데이터에 "|" 문자가 없는 것을 가정하여 조인으로 처리하였는데요, 필요 시 그룹화 부분을 커스텀 하세요.
확장메서드로 수정할 수도 있을 것이며, 데이터테이블에 기본키(public System.Data.DataColumn[] PrimaryKey { get; set; })를 추가하여, 별도의 매개변수 없이 사용해도 되겠지요?
아, 그리고 정렬은 제외했는데 이 부분도 필요하면 추가하세요.
void BeomSang()
{
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Category", typeof(string));// 키1 컬럼
dataTable.Columns.Add("Subcategory", typeof(string));// 키2 컬럼
dataTable.Columns.Add("Amount", typeof(decimal));// 소계를 계산할 컬럼
dataTable.Columns.Add("Tax", typeof(decimal));// 소계를 계산할 컬럼
dataTable.Columns.Add("Description", typeof(string));// 소계에 포함되지 않을 텍스트 컬럼
// 예제 데이터 추가
dataTable.Rows.Add("A", "a", 100, 10, "BEOMSANG");
dataTable.Rows.Add("A", "a", 200, 20, "BEOM");
dataTable.Rows.Add("A", "b", 500, 50, "SANG");
dataTable.Rows.Add("B", "b", 150, 15, "BS");
dataTable.Rows.Add("B", "b", 50, 5, "SB");
DataTable result = AddTotal(dataTable, new string[] { "Category", "Subcategory" });
}
public static DataTable AddTotal(DataTable _dataTable, string[] _keyColumns)
{
// 전체 합계 행 추가
DataRow totalRow = _dataTable.NewRow();
foreach (DataColumn column in _dataTable.Columns)
{
if (_keyColumns.Contains(column.ColumnName))
{
totalRow[column.ColumnName] = "합계";
}
else
{
if (column.DataType == typeof(decimal))
{
totalRow[column.ColumnName] = _dataTable.Compute($"SUM({column.ColumnName})", string.Empty);
}
}
}
_dataTable.Rows.Add(totalRow);
// 그룹별 소계 계산 변수
var groups = _dataTable.AsEnumerable()
.Where(row => _keyColumns.Any(col => !row[col].ToString().EndsWith("합계"))) // 모든 키 컬럼에 대해 "합계"가 포함되지 않은 행만 필터링
.GroupBy(row => string.Join("|", _keyColumns.Select(col => row[col].ToString()))); // 키로 그룹화
// 그룹별 소계 행 추가
foreach (var group in groups)
{
var keys = group.Key.Split('|');
DataRow subTotalRow = _dataTable.NewRow();
for (int i = 0; i < _keyColumns.Length; i++)
{
subTotalRow[_keyColumns[i]] = $"{keys[i]} 소계";
}
foreach (DataColumn column in _dataTable.Columns)
{
if (!_keyColumns.Contains(column.ColumnName) && column.DataType == typeof(decimal))
{
subTotalRow[column.ColumnName] = group.Sum(row => row.Field<decimal>(column.ColumnName));
}
}
_dataTable.Rows.Add(subTotalRow);
}
return _dataTable;
}
