tags:

views:

167

answers:

2

I have 2 lists and I need to combine the joining values from A and B, but also include the values from A and B that don't match the join.

class TypeA
{
    public string Key { get; set; }
    public int ValueA { get; set; }
}

class TypeB
{
    public string Key { get; set; }
    public int ValueB { get; set; }
}

class TypeAB
{
    public string Key { get; set; }
    public int ValueA { get; set; }
    public int ValueB { get; set; }
}

var listA = new List<TypeA>
{
    new TypeA { Key = "one", Value = 1 },
    new TypeA { Key = "two", Value = 2 },
};

var listB = new List<TypeB>
{
    new TypeB { Key = "two", Value = 2 },
    new TypeB { Key = "three", Value = 3 },
};

I want these lists combined to equal this:

var listAB = new List<TypeAB>
{
    new TypeAB { Key = "one", ValueA = 1, ValueB = null },
    new TypeAB { Key = "two", ValueA = 2, ValueB = 2 },
    new TypeAB { Key = "three", ValueA = null, ValueB = 3 },
};

What is a Linq statement that will do this? I've been playing around and can't quite get there. I can get almost there by doing a left outer join on A to B and Union that to a left outer join on B to A, but I get duplicate Intersection values.

Update

Here is what I did based on George's answer:

var joined =
    ( from a in listA
      join b in listB
       on a.Key equals b.Key
       into listBJoin
      from b in listBJoin.DefaultIfEmpty( new TypeB() )
      select new TypeAB
      {
       Key = a.Key,
       ValueA = a.ValueA,
       ValueB = b.ValueB,
      } ).Union(
     from b in listB
     where !listA.Any( d => d.Key == b.Key )
     select new TypeAB
     {
      Key = b.Key,
      ValueB = b.ValueB,
     }
     ).ToList();
+1  A: 

Edit: Fixed up. This is sloppy but it should work.

    listA  //Start with lisA
.Where(a=>!listB.Any(b=>a.Key == b.Key))  // Remove any that are duplicated in listB
.Select(a => new TypeAB() { Key=a.Key, ValueA=a.Value})  // Map each A to an AB
.Union(
  listB.Select(b => {
      var correspondingA = listA.FirstOrDefault(a => a.Key == b.Key);  //Is there an a that corresponds?
    return new TypeAB() { Key=b.Key, 
      ValueB=b.Value,  //Value B is easy
      ValueA= correspondingA!=null ? (int?)correspondingA.Value : null  //If there is an A than map its value
    };
  })
)

As an aside, if you're using this as some sort of domain operation, TypeA and TypeB should probably be based on some sort AorBIsAConceptThatHasMeaningInTheDomain base class. That's just a general rule whenever you find yourself combining lists. If no such concept exists, then you probably don't need to be combining the lists.

On the other hand, if you're doing this as part of a mapping - such as mapping domain objects to UI - you might be able to simplify your code somewhat by using anonymous types instead of a TypeAB class. (Or maybe not, this one is up to personal preference)

Edit Edit Here's a slightly more intellectually interesting answer using hashes

  var listAB = listA.Cast<object>().Union(listB.Cast<object>()).ToLookup(x => x is TypeA ? (x as TypeA).Key : (x as TypeB).Key)
    .Select(kv => {
     var a = kv.FirstOrDefault(x => x is TypeA) as TypeA;
     var b = kv.FirstOrDefault(x => x is TypeB) as TypeB;
     return new TypeAB() {
      Key = kv.Key,
      ValueA = a != null ? (int?)a.Value : null,
      ValueB = b != null ? (int?)b.Value : null
     };
    }).ToList();
George Mauer
This produces incorrect result in example case with key == "two". In fact, it will have ValueA == null.
Alex Bagnolini
Good point, missed that Req, hmm what happens if there are multiple TypeAs with the same key in listA?
George Mauer
Your answer gave me an idea on not including the already included results from listB after a LEFT OUTER JOIN from listA to listB.
Josh Close
I updated my question with the solution that I'm using based on your answer. Thanks.
Josh Close
Here is the test for it: http://codepaste.net/p8roia
George Mauer
+2  A: 

For exactly this scenario, in our project we use an extension method called Merge.

public static class Extensions
{
    public static IEnumerable<TResult> Merge<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftList, 
        IEnumerable<TRight> rightList, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector, 
        Func<TKey, IEnumerable<TLeft>, IEnumerable<TRight>, TResult> combiner)
    {
        var leftLookup = leftList.ToLookup(leftKeySelector);
        var rightLookup = rightList.ToLookup(rightKeySelector);

        var keys = leftLookup.Select(g => g.Key).Concat(rightLookup.Select(g => g.Key)).Distinct();
        return keys.Select(key => combiner(key, leftLookup[key], rightLookup[key]));        
    }
}

You can use Merge like this

var listAB = listA.Merge(
    listB,
    a => a.Key,
    b => b.Key,
    (key, aItems, bItems) => new TypeAB 
    { 
        Key = key, 
        ValueA = aItems.Select(a => (int?)a.Value).SingleOrDefault(), 
        ValueB = bItems.Select(b => (int?)b.Value).SingleOrDefault()
    });
Geert Baeyaert
looks familiar ;-)
jeroenh