【C#】String型DataTableに対してComputeメソッド【集約計算】

C#

はい、本日はCSVをデータテーブルに取込Computeを行おうとした所、
追加情報:集約関数 Sum() および型 : String の使用が無効です。のエラーが出た時に行った無理やりな解決策の話し。

スポンサーリンク

そもそもComputeメソッドとは?


DataTableの特定列の MAX,MIN,SUM,AVE等の集約計算を行う時に使用。
戻り値がObject型なのは注意点。

Compute(“Sum(カラム名)”, “カラム名=XXX”)
パラメータ1 expression
型 : System.String

最初に計算する式を入れ、次にフィルタを掛ける感じ。
特にフィルターが必要ない場合はnullで良し!対象が存在しない場合はnullを返します。

MAXやMIN、平均値等も取得が出来て、完全一致以外も使える便利な奴。

問題発生個所と原因


そんな便利なComputeですが、IntやらDecimalやらとにかく数値のカラムじゃなきゃ使えません。
計算するんだから当たり前ですけども。

なので全部文字列で突っ込んだデータテーブルに対して、以下みたいな事をすると表題のエラーが発生します。

コード部分

// カラム数分ループ
for (int Cnt = Dt.Columns.Count; Cnt > 0; Cnt--)
{
    // 全部数値かどうか判断するフラグ
    bCheckFlg = false;
    
    // レコード数分ループ
    for (int Cnt2 = 0; Cnt2 < Dt.Rows.Count; Cnt2++)
    {
        // 日付型または数値以外の場合
        if (DateTime.TryParse(Dt.Rows[Cnt2][Cnt-1].ToString(), out wkDate) == true ||
            int.TryParse(Dt.Rows[Cnt2][Cnt-1].ToString(), out wkNum) == false)
        {
            // 文字列や日付なのでフラグを立てて脱出
            bCheckFlg = true;
            break;
        }
    }
    // 全部数値だった場合
    if (bCheckFlg == false)
    {
        // カラム名取得
        strColName = Dt.Columns[Cnt - 1].ColumnName.ToString();
        // カラム名とその金額をため込んでく
        strPay = strPay + strColName + ":" + Dt.Compute("Sum([" + strColName + "])", null).ToString()+ "円\n";
    }
}

カラム毎にレコードを舐めてって全て数値ならbCheckFlgをTrueとし、その金額を合計しstrPayにため込んで返したかった訳です。

原因はカラムのデータ型のせい!

このまま上のコードを実行してしまうと、
ここで例の敵、追加情報:集約関数 Sum() および型 : String の使用が無効です。が登場します。

strPay = strPay + strColName + ":" + Dt.Compute("Sum([" + strColName + "])", null).ToString()+ "円\n";

原因はデータ型をStringで定義してるから(そう取り込んでしまってるから)です。
それにより文字列の計算なんて出来ません!と怒られる訳です。

実際に行った解決策


じゃあどうするか?既に値が入っているデータテーブルに対して型変換は出来ないし、
共通のCSV取込部分もいじれない、ので今回はこうしました。

コード部分

// 追加① 集約計算専用のダミーカラムを作る
Dt.Columns.Add("ダミー", Type.GetType("System.Int32"));
// カラム数分ループ
for (int Cnt = Dt.Columns.Count; Cnt > 0; Cnt--)
{
    // 全部数値かどうか判断するフラグ
    bCheckFlg = false;
    
    // レコード数分ループ
    for (int Cnt2 = 0; Cnt2 < Dt.Rows.Count; Cnt2++)
    {
        // 日付型または数値以外の場合
        if (DateTime.TryParse(Dt.Rows[Cnt2][Cnt-1].ToString(), out wkDate) == true ||
            int.TryParse(Dt.Rows[Cnt2][Cnt-1].ToString(), out wkNum) == false)
        {
            // 文字列や日付なのでフラグを立てて脱出
            bCheckFlg = true;
            break;
        }
        // 追加② 数値判定だった場合ダミーカラムに数値を入れる
        else
        {
            Dt.Rows[Cnt2]["ダミー"] = wkNum;
        }
    }
    // 全部数値だった場合
    if (bCheckFlg == false)
    {
        // カラム名取得
        strColName = Dt.Columns[Cnt - 1].ColumnName.ToString();
        // 変更① カラム名とその金額をため込んでく → ダミーカラムを見る
        strErrMsg = strErrMsg + "発生金額:" + Dt.Compute("Sum([ダミー])", null).ToString() + "円\n";
    }
}

対象のカラムで計算を行わずに専用のカラムを作ってやった。金額以外に元のデータテーブルも返す必要がある場合は、

Dt.Columns.Remove("ダミー");

で消した上で返せば問題ないかと。
今回は共通部分がいじれない&データベースから引っ張って来る訳じゃないので、
こんな感じで逃げましたけども、もちろん可能ならテーブルの設計やSQLの見直し、データのとり方の部分で修正すべきです。
まぁ致し方ない時はこんな感じで逃げれるよって話(ᵔᴥᵔ)

特定カラムのMAX値等を取りたい場合も要注意


例えばこんなデータテーブル(IDも文字列型)があったとして↓

IDNAME
1山田
2田中
3唐沢
4渡辺
5別府
6府川
7綿貫
8木下
9谷口
10中鉢
11千鳥

上記のIDのMAX値を取得したい場合に、

int nMax = int.Parse(Dt.Compute("Max([ID])", null).ToString())

みたいにするとビルドは通りますが正しく取得できません。平気な顔して9を返却してきます。

なのでこういった全部文字列型だけど本当は数値型もあって集約計算したい 場合は、Computeでは無くLINQでの集約計算を行うのがベストです(ᵔᴥᵔ)

コメント