Reputation: 13509
I have a list of objects with a property that can be used to partition the objects into pairs. I know in advance that each object is part of a pair.
Here is an example to illustrate the situation:
Let's say my list is as follows:
List<Shoe> shoes = new List<Shoe>();
shoes.Add(new Shoe { Id = 19, Brand = "Nike", LeftOrRight = LeftOrRight.L });
shoes.Add(new Shoe { Id = 29, Brand = "Nike", LeftOrRight = LeftOrRight.R });
shoes.Add(new Shoe { Id = 11, Brand = "Nike", LeftOrRight = LeftOrRight.L });
shoes.Add(new Shoe { Id = 60, Brand = "Nike", LeftOrRight = LeftOrRight.R });
shoes.Add(new Shoe { Id = 65, Brand = "Asics", LeftOrRight = LeftOrRight.L });
shoes.Add(new Shoe { Id = 82, Brand = "Asics", LeftOrRight = LeftOrRight.R });
I would like to output these shoes as pairs, like so:
Pair: Id: 19, Brand: Nike, LeftOrRight: L Id: 29, Brand: Nike, LeftOrRight: R Pair: Id: 11, Brand: Nike, LeftOrRight: L Id: 60, Brand: Nike, LeftOrRight: R Pair: Id: 65, Brand: Asics, LeftOrRight: L Id: 82, Brand: Asics, LeftOrRight: R
Note that an individual shoe can only exist as part of a single pair.
I have tried the following code to group the shoes, but it is clearly missing the pairs:
var pairsByBrand = shoes.GroupBy(s => s.Brand);
foreach (var group in pairsByBrand)
{
Console.WriteLine("Pair:");
foreach (var shoe in group)
{
Console.WriteLine(shoe);
}
Console.WriteLine();
}
What statements can be used to group these items into pairs?
Upvotes: 7
Views: 2501
Reputation: 50245
var shoesByBrand = shoes.GroupBy(s => s.Brand);
foreach (var byBrand in shoesByBrand)
{
var lefts = byBrand.Where(s => s.LeftOrRight == LeftOrRight.L);
var rights = byBrand.Where(s => s.LeftOrRight == LeftOrRight.R);
var pairs = lefts.Zip(rights,(l, r) => new {Left = l, Right = r});
foreach(var p in pairs)
{
Console.WriteLine("Pair: {{{0}, {1}}}", p.Left.Id, p.Right.Id);
}
Console.WriteLine();
}
Note: Zip will only pair up as much as it can. If you have extra rights or lefts they won't get reported.
Upvotes: 3
Reputation: 437854
One way to do it:
var pairs = shoes.GroupBy(s => s.Brand)
.Select(g => g.GroupBy(s => s.LeftOrRight));
.SelectMany(Enumerable.Zip(g => g.First(), g => g.Last(),Tuple.Create));
This is possibly an improvement on my initial idea (which has been nicely implemented by Thom Smith) in that for each brand of shoes it splits them up in left and right shoes by iterating the collection only once. Gut feeling says it should be faster if there are brands with lots of shoes.
What it does is group the shoes by brand, then within each brand by left/right. It then proceeds to randomly match left shoes of each brand with right shoes of the same, doing so for all brands in turn.
Upvotes: 2
Reputation: 14086
Pure functional LINQ, using SelectMany
and Zip
, yielding an IEnumerable
of Tuple
s:
IEnumerable<Tuple<Shoe, Shoe>> pairs = shoes
.GroupBy(shoe => shoe.Brand)
.SelectMany(brand=>
Enumerable.Zip(
brand.Where(shoe=>shoe.LeftOrRight == LeftOrRight.L),
brand.Where(shoe=>shoe.LeftOrRight == LeftOrRight.R),
Tuple.Create
)
);
Upvotes: 9