如何在LINQ中无聚合转置ObservableCollection<DataValue>数据?
问题描述
我将每个采集参数的数据存储在ObservableCollection<DataValue>集合中,DataValue类定义如下:
public sealed class DataValue: BaseModel { private int _id; public int ID { get { return _id; } set { _id = value; NotifyPropertyChanged("ID"); } } private DateTime _timevalue; public DateTime TimeValue { get { return _timevalue; } set { _timevalue = value; NotifyPropertyChanged("TimeValue"); } } private double _value; public double Value { get { return _value; } set { _value = value; NotifyPropertyChanged("Value"); } } private string _name; public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged("Name"); } } }
从数据库层获取的数据格式如下:
| ID | Date | Value | Name |
|---|---|---|---|
| 1 | 2019-11-17 20:00:00 | 200.05 | Name1 |
| 1 | 2019-11-17 20:03:00 | 210.05 | Name1 |
| 1 | 2019-11-17 20:06:00 | 225.45 | Name1 |
| ... | ... | ... | ... |
| 2 | 2019-11-17 20:00:00 | 1500 | Name2 |
| 2 | 2019-11-17 20:03:00 | 1600 | Name2 |
| 2 | 2019-11-17 20:06:00 | 1550 | Name2 |
| ... | ... | ... | ... |
| 3 | 2019-11-17 20:00:00 | 0.105 | Name3 |
| 3 | 2019-11-17 20:03:00 | 0.267 | Name3 |
| 3 | 2019-11-17 20:06:00 | 0.560 | Name3 |
| ... | ... | ... | ... |
希望通过LINQ仅执行转换操作(不使用聚合)将其转置,使每个参数Name1、Name2、Name3……NameN成为列,得到如下目标格式:
| Date | Name1 | Name2 | Name3 | Name[n] |
|---|---|---|---|---|
| 2019-11-17 20:00:00 | 200.05 | 1500 | 0.105 | xxx.xx |
| 2019-11-17 20:03:00 | 210.05 | 1600 | 0.267 | xxx.xx |
| 2019-11-17 20:06:00 | 225.45 | 1550 | 0.560 | xxx.xx |
| ... | ... | ... | ... | ... |
请问该如何实现这一需求?
解决方案
这是个典型的数据转置(透视)需求,刚好可以用LINQ的分组+映射来实现——完全不需要聚合操作,符合你的要求。根据你是否提前知道参数名称,有几种不同的实现方式:
1. 已知参数名称时:用匿名类型直接构造
如果你的参数列表是固定的(比如确定只有Name1、Name2、Name3),直接按时间分组后,在每个分组里提取对应名称的值就行:
var pivotedResult = yourDataCollection .GroupBy(dv => dv.TimeValue) // 按时间戳分组,每个组对应一行结果 .Select(group => new { Date = group.Key, Name1 = group.FirstOrDefault(dv => dv.Name == "Name1")?.Value ?? 0.0, Name2 = group.FirstOrDefault(dv => dv.Name == "Name2")?.Value ?? 0.0, Name3 = group.FirstOrDefault(dv => dv.Name == "Name3")?.Value ?? 0.0 // 有更多固定参数就继续加对应的行 });
这里用FirstOrDefault是为了避免某个时间点缺失某参数数据时抛出异常,默认值(这里写的0.0)你可以根据业务调整成null或者其他值。
2. 参数名称动态时:用字典存储
如果参数是动态生成的(比如可能随时新增NameN),强类型就不太灵活了,这时用字典来存每行的键值对最合适:
// 先获取所有唯一的参数名称 var allParameterNames = yourDataCollection .Select(dv => dv.Name) .Distinct() .ToList(); var pivotedResult = yourDataCollection .GroupBy(dv => dv.TimeValue) .Select(group => { var rowDict = new Dictionary<string, object> { ["Date"] = group.Key // 先把时间列放进去 }; // 遍历所有参数名称,填充对应的值 foreach (var name in allParameterNames) { rowDict[name] = group.FirstOrDefault(dv => dv.Name == name)?.Value ?? 0.0; } return rowDict; });
这种方式不管参数数量怎么变都能自动适配,缺点是后续取值需要通过键名,没有强类型的编译提示。
3. 想要动态列+类属性体验:用ExpandoObject
如果既想要动态列的灵活性,又想像访问类属性一样取值,可以用ExpandoObject:
var allParameterNames = yourDataCollection .Select(dv => dv.Name) .Distinct() .ToList(); var pivotedResult = yourDataCollection .GroupBy(dv => dv.TimeValue) .Select(group => { dynamic dynamicRow = new ExpandoObject(); dynamicRow.Date = group.Key; // 把ExpandoObject转成字典来添加动态属性 var rowDict = (IDictionary<string, object>)dynamicRow; foreach (var name in allParameterNames) { rowDict[name] = group.FirstOrDefault(dv => dv.Name == name)?.Value ?? 0.0; } return dynamicRow; });
之后你就可以像dynamicRow.Date、dynamicRow.Name1这样访问属性了,不过要注意动态类型没有编译时检查,使用时要小心空值或者不存在的属性。
这几种方式都是纯LINQ转换,没有用Sum、Avg这类聚合操作,完全满足你的要求。
内容的提问来源于stack exchange,提问作者amaranth
相关产品推荐
相关产品推荐

