IEnumerable<T>をピボット集計するライブラリ書いた


IEnumerableをピボット集計するライブラリ書いた

※MVC5用

世間がコロナコロナうっせーのでムシャクシャしてやった。

よくある話

「ここにこういう表があるじゃろ」

「これをピボットみたいにしてほしい。Excelみたいに。Excelみたいに。」

すげぇ面倒。

面倒ポイント

  • 列数が不定なのでタイトル行と明細行でそれぞれ横方向のループで描画する必要がある
  • 列や行をrowspanやcolspanで結合させるためにそのセルの下位に位置するセルを計算する必要がある。
  • 行方向だと特に前の行でセルが結合済みだったりするのでどの列ヘッダから描画すべきかフラグ管理が鬱陶しい。
  • 小計や合計を出そうとすると更に倍ぐらい面倒くさい。

書いた

qyen/ToPivotTable: Convert IEnumerable to pivot table on C#

model.pivot = DB.ToPivotTable(
        new List<PivotColumn<MockData>>() {
            // carの頭文字を逆順で集計 
            new PivotColumn<MockData>("initial",(t)=>t.car.Substring(0,1),(t)=>t.car.Substring(0,1)){
                Order=PivotOrder.Descending,
            },
            new PivotColumn<MockData>("car"),
        },
        new List<PivotColumn<MockData>>() {
            //スペース区切りのJobの最初の単語をカテゴリとして集計
            new PivotColumn<MockData>("category",(t)=>t.Job.Split(' ').First(),(t)=>t.Job.Split(' ').First()),
            //スペース区切りのJobの2番め以降をJobとして集計
            new PivotColumn<MockData>("Job",(t)=>string.Join(" ",t.Job.Split(' ').Skip(1))),
        },
        new List<PivotMeasure<MockData>>() {
            // 集計するのはcashの平均値
            PivotMeasure<MockData>.Average("Avg.Cash",(t)=>t.cash),
        }
    );

しくみ

ざっくりとこう分けて

列ヘッダだけで見ると

こういうデータ構成になってる。

データ構成だけに着目するとColumn(0)..Column(n)をキーにしたツリー構造

と、見ることができる。

行列ヘッダ

LinqのIEnumerable<T>.GroupBy()IEnumerable<IGrouping<T>>を返し、IGroupingはIEnumerableのサブクラスであるため、

foreach (var group0 in source.GroupBy(<Column0>)){
  foreach (var group1 in group0.GroupBy(<Column1>)){
    foreach (var group2 in group1.GroupBy(<Column2>)){
    : 
    }
  }
} 

という形で深堀りできるので、再帰を使って一気にツリーを生成してる。
ToPivotTable/PivotTable.cs at master · qyen/ToPivotTable

構造的に行も列も同じ。

measure

measureは

  • Tから値を取り出すValueGetter
  • 値を集計する aggregater function

で成り立ってる。

Pivotから見れば行列の座標から抽出したリストに対して集計して値を出す部分を委譲するのがこのMeasure。

値の取り出し

この図のように、あるセルを表す集合ListOfCellはそのセルの座標となる各ヘッダーセルの値で元集合をフィルターしたものになる。
そこにMeasureを通すと出力すべき値が取り出せる。

小計や合計の取り出し

小計や合計の値は、そのセルの座標となる各ヘッダーセルのうち合計ヘッダーセルでないものの値で元集合をフィルターしたものから算出できる。

今後とか

列定義と集計すべき値が定義としてあるんだから先にGroupBy(Column(0)..Column(n)).Select(Column(0)..Column(n),Measure(0)..Measure(m))みたいに集計しちゃえばCPUに優しい感じになりそうなんだけど、どうやって実装したもんか。

コロナがすべてわるい。