7

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?

Ryan Kohn
  • 13,079
  • 14
  • 56
  • 81
  • 3
    What's preventing the {19,60} pair? – Austin Salonen Sep 07 '12 at 15:02
  • @AustinSalonen That pair would be acceptable also. The only restriction is that the pairs must be of the same brand, with one left and one right shoe. – Ryan Kohn Sep 07 '12 at 15:03
  • @Ryan Kohn You need another Property, for example ProductName and each L and R, that should be a pair, must have the same ProductName. Then group by ProductName not by Brand, because by Brand you have 4 items for group Nike and 2 for group Asics – Alberto León Sep 07 '12 at 15:08

3 Answers3

9

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
        )
    );
Thom Smith
  • 13,916
  • 6
  • 45
  • 91
  • 1
    Nice solution, but again, aren't ANY pairs (with left and right) that are the same brand able to be grouped? With this solution Austin's you will get different pairings depending on the order the shoes show up in the list. So, if I add shoes 11 and then 60, then they will form a pair, but if I add 11 and *29* first, then those would form a pair. Is that what's expected? We need the OP to weigh in. – aquinas Sep 07 '12 at 15:27
  • 1
    @aquinas: The OP has already clarified that any two left shoes of the same brand are identical for the purposes of this discussion. – Jon Sep 07 '12 at 15:30
  • 1
    My interpretation was that each shoe should be present exactly once in the output, and that the order was irrelevant as long as every pairing was valid. The OP may have been a bit vague on this point. – Thom Smith Sep 07 '12 at 15:31
3
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.

Ryan Kohn
  • 13,079
  • 14
  • 56
  • 81
Austin Salonen
  • 49,173
  • 15
  • 109
  • 139
  • 1
    +1 i need to read up on GroupBy and Zip against, it looks strong :) Btw, did you mean Console.ReadKey(); at the end instead? – radbyx Sep 07 '12 at 15:18
  • I *think* though that *any* left and right can go together as long as the brand is the same. From the OP: "That pair would be acceptable also. The only restriction is that the pairs must be of the same brand, with one left and one right shoe." – aquinas Sep 07 '12 at 15:23
  • 1
    @aquinas: If you start listing all _possible_ pairs, the output explodes with relatively few shoes. There was some ambiguity in the question but your answer and mine should cover it. – Austin Salonen Sep 07 '12 at 15:27
  • While Thom Smith's answer was more succinct, I ended up using this one because the code was clearer. – Ryan Kohn Sep 07 '12 at 16:25
2

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.

Community
  • 1
  • 1
Jon
  • 428,835
  • 81
  • 738
  • 806