给定序列:
["1","A","B","C","2","F","K","L","5","6","P","I","E"]
数字代表我标识为标题的项目,而字母代表我标识为数据的项目。我想将它们关联到这样的组中。
1:A,B,C
2:F,K,L
5:
6:P,I,E
我可以使用枚举器上的 foreach 或 while 循环轻松实现这一点,但是有没有一种 LINQ 的方法来实现这一点?这是我的领域中反复出现的模式。
给定序列:
["1","A","B","C","2","F","K","L","5","6","P","I","E"]
数字代表我标识为标题的项目,而字母代表我标识为数据的项目。我想将它们关联到这样的组中。
1:A,B,C
2:F,K,L
5:
6:P,I,E
我可以使用枚举器上的 foreach 或 while 循环轻松实现这一点,但是有没有一种 LINQ 的方法来实现这一点?这是我的领域中反复出现的模式。
这是 LINQ 的解决方案。虽然有点复杂。可能有一些技巧的空间。它看起来并不那么糟糕,但是使用 foreach 循环可以提高可读性。
int lastHeaderIndex = default(int);
Dictionary<string, IEnumerable<string>> groupedItems =
items.Select((text, index) =>
{
int number;
if (int.TryParse(text, out number))
{
lastHeaderIndex = index;
}
return new { HeaderIndex = lastHeaderIndex, Value = text };
})
.GroupBy(item => item.HeaderIndex)
.ToDictionary(item => item.FirstOrDefault().Value,
item => item.Skip(1).Select(arg => arg.Value));
foreach
循环int.TryParse
应该有帮助。来自 LINQ 的“GroupBy”在这里没有多大帮助。
您可以使用折叠:
var aggr = new List<Tuple<Int,List<String>>>();
var res = sequence.Aggregate(aggr, (d, x) => {
int i;
if (Int32.TryParse(x, out i)) {
var newDict = d.Add(new Tuple(i, new List<string>()));
return newDict;
}
else {
var newDict = d[d.Count - 1].Item2.Add(x);
return newDict;
}
}).ToDictionary(x => x.Item1, x => x.Item2);
However, this doesn't look so nice, since there's lacking support for immutable values. Also, I couldn't test this right now.
Since this a common pattern in your domain, consider streaming the results instead of gathering them all into a large in-memory object.
public static IEnumerable<IList<string>> SplitOnToken(IEnumerable<string> input, Func<string,bool> isSplitToken)
{
var set = new List<string>();
foreach(var item in input)
{
if (isSplitToken(item) && set.Any())
{
yield return set;
set = new List<string>();
}
set.Add(item);
}
if (set.Any())
{
yield return set;
}
}
Sample usage:
var sequence = new[] { "1", "A", "B", "C", "2", "F", "K", "L", "5", "6", "P", "I", "E" };
var groups = SplitOnToken(sequence, x => Char.IsDigit(x[0]));
foreach (var @group in groups)
{
Console.WriteLine("{0}: {1}", @group[0], String.Join(" ", @group.Skip(1).ToArray()));
}
output:
1: A B C
2: F K L
5:
6: P I E
Here's what I ended up using. Pretty much the same structure as phg's answer.
Basically, it is an aggregate function that maintains a Tuple containing: 1: the accummulated data. 2: state of the parser.
The aggregating function does an if-else to check if currently examined item is a group header or a regular item. Based on this, it updates the datastore (last part of the tuple) and/or changes the parser state (first part of the tuple).
In my case, the parser state is the currently active list (that upcoming items shall be inserted into).
var sequence = new[]{ "1","A","B","C","2","F","K","L","5","6","P","I","E"};
var aggr = Tuple.Create(new List<string>(), new Dictionary<int,List<string>>());
var res = sequence.Aggregate(aggr, (d, x) => {
int i;
if (Int32.TryParse(x, out i))
{
var newList = new List<string>();
d.Item2.Add(i,newList);
return Tuple.Create(newList,d.Item2);
} else
{
d.Item1.Add(x);
return d;
}
},d=>d.Item2);