简体   繁体   中英

LINQ merging two lists(full outer join on composite keys)

I have two lists

 IEnumerable<Citrus> grapefruit = citrusList.Where(x => x.IsSmall == false);
 IEnumerable<Citrus> tangerines = citrusList.Where(x => x.IsSmall == true);

I want to put my all of Citrus in a PackingContainer, but I want to first make tangelos-- a combination of grapefruit and tangerine-- from my grapefruits and tangerines where the Citrus.Color = orange, Citrus.flavor = very tangy, Citrus.Texture = grainy and the Citrus.State = ripe

Right now I have nested foreach loops that check

 foreach (Citrus fruit in grapefruit)
 {
    foreach (Citrus fruitToo in tangerines)
    {
       PackingContainer container = new PackingContainer();
       if (fruit.Color == fruitToo.Color && 
           fruit.Flavor == fruitToo.Flavor && 
           fruit.Texture == fruitToo.Texture && 
           fruit.State == fruitToo.State)
           { 
              Tangelo tangy = new Tangelo(fruit.Color, fruit.Flavor, fruit.Texture, fruit.State, "A tangelo", new Decimal(0.75);
              container.Add(tangy);
           }
     }
  }

But I'm sure there's a better way to do this. I want to essentially do a full outer join (union all grapefruit and tangerines, but make tangelos out of the intersection). My end goal is to have a PackingContainer that has some grapefruit, some tangerines, and some tangelos in it. I'm sure there's a more elegant way to do that in LINQ.

...but I can't figure it out from http://msdn.microsoft.com/en-us/library/bb907099.aspx and http://msdn.microsoft.com/en-us/library/bb384063.aspx and it's not exactly a Union because I'm modifying intersecting members (http://msdn.microsoft.com/en-us/library/bb341731.aspx)

Little help?

It actually sounds like you need an inner join, not an outer. Your nested for loops are actually performing the equivalent of an inner join. At any rate:

grapefruit
 .Join(
  tangerines,
  x => new { Color = x.Color, Flavor = x.Flavor, Texture = x.Texture, State = x.State },
  x => new { Color = x.Color, Flavor = x.Flavor, Texture = x.Texture, State = x.State },
  (o,i) => new Tangelo(o.Color, o.Flavor, o.Texture, o.State, "A tangelo", new Decimal(0.75))
 ).Map(x => container.Add(x));

Where 'Map' is a 'ForEach'-esque extension method for IEnumerables:

public static void Map<T>(this IEnumerable<T> source, Action<T> func)
{
    foreach (T i in source)
        func(i);
}

EDIT: Fair enough. From the question it sounded like you were only interested in the tangelos. Here is an Outer Join version (This is untested, so let me know if anything doesn't work!):

var q =
from fruit in grapefruit.Select(x => new { x.Color, x.Flavor, x.Texture, x.State })
   .Union(tangerines.Select(x => new { x.Color, x.Flavor, x.Texture, x.State }))
join g in grapefruit on fruit equals new { g.Color, g.Flavor, g.Texture, g.State } into jg
from g in jg.DefaultIfEmpty()
join t in tangerines on fruit equals new { t.Color, t.Flavor, t.Texture, t.State } into jt
from t in jt.DefaultIfEmpty()
select  (g == null ? 
   t as Citrus : 
   (t == null ? 
    g as Citrus : 
    new Tangelo(g.Color, g.Flavor, g.Texture, g.State, "A tangelo", new Decimal(0.75)) as Citrus
   )
  );

And then you can add them to the container using the map method or the AddRange method from David B's answer.

You don't want a full outer join for that, or you'd wind up making some tangelos without Grapefruit and some tangelos without Tangerines.

here's an inner join.

List<Tangelo> tangelos = (
from fruit in grapefruit
join fruitToo in tangerines
  on new {fruit.Flavor, fruit.Color, fruit.Flavor, fruit.State}
  equals new {fruitToo.Flavor, fruitToo.Color, fruitToo.Flavor, fruitToo.State}
select new Tangelo(fruit.Color, fruit.Flavor, fruit.Texture, fruit.State,
  "A tangelo", new Decimal(0.75))
).ToList()

Even that is suspect. What if 3 Grapefruit match 1 Tangerine, then you get 3 Tangelos!

Try this filtering to get only one Tangelo per tangerine:

List<Tangelo> tangelos = (
from fruit in tangerines
where grapefruit.Any(fruitToo => 
  new {fruit.Flavor, fruit.Color, fruit.Flavor, fruit.State}
  == new {fruitToo.Flavor, fruitToo.Color, fruitToo.Flavor, fruitToo.State})
select new Tangelo(fruit.Color, fruit.Flavor, fruit.Texture, fruit.State,
  "A tangelo", new Decimal(0.75))
).ToList()

Of course, once you have a List of Tangelos, you can pack them by

container.AddRange(tangelos);

I think this does the trick:

var cs = from c in citrusList
         group c by new { c.Color, c.Flavor, c.Texture, c.State } into gcs
         let gs = gcs.Where(gc => gc.IsSmall == false)
         let ts = gcs.Where(gc => gc.IsSmall == true)
         let Tangelos = gs
            .Zip(ts, (g, t) =>
                new Tangelo(g.Color, g.Flavor, g.Texture, g.State,
                    "A tangelo", new Decimal(0.75)))
         select new
         {
             gcs.Key,
             Grapefruit = gs.Skip(Tangelos.Count()),
             Tangerines = ts.Skip(Tangelos.Count()),
             Tangelos,
         };

var container = new PackingContainer();

container.AddRange(from c in cs
                   from f in c.Grapefruit
                       .Concat(c.Tangerines)
                       .Concat(c.Tangelos.Cast<Citrus>())
                   select f);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM