DataTableの集計を行う(.NET 1.1)

DataTable.Computeメソッドを利用すればDataTable内で集計が行えるらしい。
DataTable.Compute メソッド(System.Data)

とりあえずSUMを行うサンプルを書いてみた。
.Net framework2.0以降だと、Mergeとか便利なメソッドが使えるのでまた違った書き方になるだろう。

 ''' -----------------------------------------------------------------------------
    ''' <summary>
    ''' DataTableに対して集計を行う
    ''' </summary>
    ''' <param name="dtOriginal">集計対象となるテーブル</param>
    ''' <param name="groupBy">Group By指定するカラム名</param>
    ''' <returns>集計済テーブル</returns>
    ''' <remarks>
    ''' 引数:dtOriginalに存在する列のうち、groupByで指定されなかった列は全てSummaryする。
    ''' 数値型でない項目はgroupByで指定しないとDataExceptionが発生する。
    ''' </remarks>
    ''' -----------------------------------------------------------------------------
    Private Function DataTableSummary(ByVal dtOriginal As DataTable, ByVal ParamArray groupBy() As String) As DataTable

        ' 引数チェック
        For Each key As String In groupBy
            If (Not dtOriginal.Columns.Contains(key)) Then
                Throw New Exception("groupByで指定されたカラムがdtOriginalに存在しません")
            End If
        Next

        ' 返却用テーブル。引数で渡されたテーブルのクローンを作成しておく。
        Dim dtFilter As DataTable = dtOriginal.Clone()

        ' 引数:groupByで指定された列を返却用テーブルのキーとして設定する
        ' MEMO : ArrayList.ToArray()の結果をDataColumn配列にキャストしたものをPrimaryKeyに設定するとなぜか実行時エラー
        Dim colKeys(groupBy.Length) As DataColumn
        For i As Integer = 0 To groupBy.Length - 1
            colKeys(i) = dtFilter.Columns(groupBy(i))
        Next
        dtFilter.PrimaryKey = colKeys

        ' テーブルの集計を行う
        ' DataViewを利用して、処理済みのキーを持つ行を排除しながらループする
        While dtOriginal.DefaultView.Count > 0
            Dim drv As DataRowView = dtOriginal.DefaultView(0)
            Dim filter As New System.Text.StringBuilder

            For Each key As String In groupBy
                ' フィルタ用の文字列を作成する
                ' KeyColumn1='VALUE1' AND KeyColumn2='VALUE2'……のような文字列が生成される
                If (filter.Length > 0) Then
                    filter.Append(" AND ")
                End If
                filter.Append(String.Format("{0}='{1}'", key, drv.Row(key)))
            Next

            Dim newRow As DataRow = dtFilter.NewRow
            For Each col As DataColumn In dtFilter.Columns
                If (Array.IndexOf(colKeys, col) >= 0) Then
                    ' groupByで指定された列は引数で渡されたテーブルのデータをそのまま使う
                    newRow(col.ColumnName) = drv.Row(col.ColumnName)
                Else
                    ' groupByで指定されなかった列はフィルタ文字列を使用してSUM

                    ' HACK : 数値型でないとExceptionが発生してしまう
                    '        そもそもSUM用メソッドなので、集計できない項目をGroupBy指定しないのはおかしいが。
                    newRow(col.ColumnName) = dtOriginal.Compute(String.Format("SUM({0})", col.ColumnName), filter.ToString())
                End If
            Next

            ' 集計した行を返却用テーブルに追加
            dtFilter.Rows.Add(newRow)

            ' 集計済のキーを排除するフィルタを追加
            If (dtOriginal.DefaultView.RowFilter.Length > 0) Then
                dtOriginal.DefaultView.RowFilter += String.Format(" AND (Not ({0}))", filter.ToString())
            Else
                dtOriginal.DefaultView.RowFilter += String.Format("(Not ({0}))", filter.ToString())
            End If

        End While

        dtFilter.AcceptChanges()
        Return dtFilter
    End Function

引数を工夫すれば他の集計関数(AVEとかMINとか)にも対応させられる気がする。
速度はイマイチ。