在 Pivot 中包含更多行

Posted

技术标签:

【中文标题】在 Pivot 中包含更多行【英文标题】:Include more rows in Pivot 【发布时间】:2018-10-22 12:42:35 【问题描述】:

我正在使用以下链接中的扩展方法来透视我的数据: https://techbrij.com/pivot-c-array-datatable-convert-column-to-row-linq

我将链接中的代码包含在内,以防将来有人发现此问题并且链接已失效:

public static DataTable ToPivotTable<T, TColumn, TRow, TData>(
    this IEnumerable<T> source,
    Func<T, TColumn> columnSelector,
    Expression<Func<T, TRow>> rowSelector,
    Func<IEnumerable<T>, TData> dataSelector)
        
            DataTable table = new DataTable();
            var rowName = ((MemberExpression)rowSelector.Body).Member.Name;
            table.Columns.Add(new DataColumn(rowName));
            var columns = source.Select(columnSelector).Distinct();

            foreach (var column in columns)
                table.Columns.Add(new DataColumn(column.ToString()));

            var rows = source.GroupBy(rowSelector.Compile())
                             .Select(rowGroup => new
                             
                                 Key = rowGroup.Key,
                                 Values = columns.GroupJoin(
                                     rowGroup,
                                     c => c,
                                     r => columnSelector(r),
                                     (c, columnGroup) => dataSelector(columnGroup))
                             );

            foreach (var row in rows)
            
                var dataRow = table.NewRow();
                var items = row.Values.Cast<object>().ToList();
                items.Insert(0, row.Key);
                dataRow.ItemArray = items.ToArray();
                table.Rows.Add(dataRow);
            

            return table;
        

参考链接中的示例,您会得到像这样的透视数据;

var pivotTable = data.ToPivotTable(
              item => item.Year, 
              item => item.Product,  
              items => items.Any() ? items.Sum(x=>x.Sales) : 0);

我的问题是,如何在此查询中包含更多行以返回例如 ProductCode..item =&gt; new item.Product, item.ProductCode 不起作用..


============== 编辑/2018 年 10 月 23 日 ==============


假设我的数据是这样的;

借助上述代码,我可以做到这一点;

我想要实现的是这个(额外的 col:STOCKID 或任何其他 cols);

【问题讨论】:

嗨,布赖恩,请...谢谢。 【参考方案1】:

匿名类型不能作为泛型参数传递。尝试将枢轴键定义为结构:

public struct PivotKey

    public string Product;
    public int ProductCode; // assuming your product codes are integers

这样你就可以利用结构体默认的EqualsGetHashCode 方法在所有字段的相等性和哈希码方面的实现。

然后,定义rowSelector如下:

item => new PivotKey  Product = item.Product, ProductCode = item.ProductCode

【讨论】:

感谢您的回答。我对添加一列但很多不是特别感兴趣。例如,除了 STOCKID 列之外,如果我想包含 CATEGORY、SUPPLIER 等列怎么办? 那么您还必须将这些字段添加到 PivotKey 结构中。如果您计划使用多个不同的数据透视键,则必须为每个键定义一个结构(例如:ProductPivotKey、BranchPivotKey 等)。如果你想让用户动态定义他们的主键,我的解决方案将不起作用,你需要使用表达式树来动态构建rowSelector【参考方案2】:

示例:https://dotnetfiddle.net/mXr9sh

问题似乎是从表达式中获取行名,因为它只设计用于处理一行。这可以通过这个函数来解决:

public static IEnumerable<string> GetMemberNames<T1, T2>(Expression<Func<T1, T2>> expression)

    var memberExpression = expression.Body as MemberExpression;
    if (memberExpression != null) 
    
        return new[] memberExpression.Member.Name ;
    
    var memberInitExpression = expression.Body as MemberInitExpression;
    if (memberInitExpression != null)
    
        return memberInitExpression.Bindings.Select(x => x.Member.Name);
    
    var newExpression = expression.Body as NewExpression;
    if (newExpression != null)
    
        return newExpression.Arguments.Select(x => (x as MemberExpression).Member.Name);
    

    throw new ArgumentException("expression"); //use: `nameof(expression)` if C#6 or above

一旦你有了这个功能,你可以替换这些行:

var rowName = ((MemberExpression)rowSelector.Body).Member.Name;
table.Columns.Add(new DataColumn(rowName));

有了这个:

var rowNames = GetMemberNames(rowSelector);
rowNames.ToList().ForEach(x => table.Columns.Add(new DataColumn(x)));

这种方法的一个缺点是这些列的各种值在单个列中串联返回;所以你需要从字符串中提取数据。


结果数据表:

(显示为 JSON)

[
  
    "StockId": " StockId = 65, Name = Milk ",
    "Name": "3",
    "Branch 1": "1",
    "Branch 2": "0",
    "Central Branch": null
  ,
  
    "StockId": " StockId = 67, Name = Coffee ",
    "Name": "0",
    "Branch 1": "0",
    "Branch 2": "22",
    "Central Branch": null
  
]

完整代码清单

using System;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
using Newtonsoft.Json; //just for displaying output

public class Program 

    public static void Main()
    
        var data = new[]  
            new  StockId = 65, Name = "Milk", Branch = 23, BranchName = "Branch 1", Stock = 3 ,
            new  StockId = 65, Name = "Milk", Branch = 24, BranchName = "Branch 2", Stock = 1 ,
            new  StockId = 67, Name = "Coffee", Branch = 22, BranchName = "Central Branch", Stock = 22 
        ;

        var pivotTable = data.ToPivotTable(
            item => item.BranchName, 
            item => new item.StockId, item.Name,  
            items => items.Any() ? items.Sum(x=>x.Stock) : 0);

        //easy way to view our pivotTable if using linqPad or similar
        //Console.WriteLine(pivotTable);
        //if not using linqPad, convert to JSON for easy display
        Console.WriteLine(JsonConvert.SerializeObject(pivotTable, Formatting.Indented));
    
   

public static class PivotExtensions

    public static DataTable ToPivotTable<T, TColumn, TRow, TData>(
        this IEnumerable<T> source,
        Func<T, TColumn> columnSelector,
        Expression<Func<T, TRow>> rowSelector,
        Func<IEnumerable<T>, TData> dataSelector)
    
        DataTable table = new DataTable();
        //foreach (var row in rowSelector()
        var rowNames = GetMemberNames(rowSelector);
        rowNames.ToList().ForEach(x => table.Columns.Add(new DataColumn(x)));
        var columns = source.Select(columnSelector).Distinct();

        foreach (var column in columns)
            table.Columns.Add(new DataColumn(column.ToString()));

        var rows = source.GroupBy(rowSelector.Compile())
            .Select(rowGroup => new
                    
                        Key = rowGroup.Key,
                        Values = columns.GroupJoin(
                            rowGroup,
                            c => c,
                            r => columnSelector(r),
                            (c, columnGroup) => dataSelector(columnGroup))
                    );

        foreach (var row in rows)
        
            var dataRow = table.NewRow();
            var items = row.Values.Cast<object>().ToList();
            items.Insert(0, row.Key);
            dataRow.ItemArray = items.ToArray();
            table.Rows.Add(dataRow);
        

        return table;
    
    public static IEnumerable<string> GetMemberNames<T1, T2>(Expression<Func<T1, T2>> expression)
    
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression != null) 
        
            return new[] memberExpression.Member.Name ;
        
        var memberInitExpression = expression.Body as MemberInitExpression;
        if (memberInitExpression != null)
        
            return memberInitExpression.Bindings.Select(x => x.Member.Name);
        
        var newExpression = expression.Body as NewExpression;
        if (newExpression != null)
        
            return newExpression.Arguments.Select(x => (x as MemberExpression).Member.Name);
        

        throw new ArgumentException("expression"); //use: `nameof(expression)` if C#6 or above
    


【讨论】:

以上是关于在 Pivot 中包含更多行的主要内容,如果未能解决你的问题,请参考以下文章

如何在第三个查询中包含来自其他两个表的过滤行数?

SQL问题中包含许多必要行的一张表

如何在 s-s-rS 订阅的主题行中包含报告参数?

仅显示特定行中包含特定值的行

pandas筛选dataframe数据:筛选指定字符串数据列中包含A字符或者B字符的所有数据行(包含指定字符中的至少其中一个的所有数据行)

PHP计算一列中包含相同值的MYSQL行