Ryan Kohn
Ryan Kohn

Reputation: 13509

Linq - group a list into pairs based on a property

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:


I have a list of individual shoes that I would like to group into pairs.

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

Answers (3)

Austin Salonen
Austin Salonen

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

Jon
Jon

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

Thom Smith
Thom Smith

Reputation: 14086

Pure functional LINQ, using SelectMany and Zip, yielding an IEnumerable of Tuples:

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

Related Questions