views:

820

answers:

11

I have an IEnumerable<T> and an IEnumerable<U> that I want merged into an IEnumerable<KeyValuePair<T, U>>, where the indexes of the elements joined together in the KeyValuePair are the same. Note I'm not using IList, so I don't have a count or an index for the items I'm merging. How best can I accomplish this? I would prefer a LINQ answer, but anything that gets the job done in an elegant fashion would work as well.

+2  A: 

Think about what you're asking a bit more closely here:

You want to combine two IEnumerables in which "the indexes of the elements joined together in the KeyValuePair are the same", but you "don't have a count or an index for the items I'm merging".

There's no guarantee your IEnumerables are even sorted or unsorted. There's no correlation between your two IEnumerable objects, so how can you expect to correlate them?

Welbog
@welbog: It looks like there's a misunderstanding of the question. I think that by "index" Erik meant the position of the element in the IEnumerable (1st, 2nd, etc)
Mauricio Scheffer
@mausch: an position which isn't guaranteed. Depending on the implementation, the order of the two IEnumerables might not be what is expected.
Welbog
@welbog: would it make sense to call Zip with such an enumerable? Either it doesn't make sense or the caller has to be aware of this... I don't see any other option.
Mauricio Scheffer
@mausch: my point is the problem itself cannot be solved as stated. There simply is no piece of information required to make the connection between the objects in the two IEnumerables. Your solution makes an assumption, an assumption that adds the extra information, but crashes and burns when false.
Welbog
"how can you expect to correlate them" - valid point, but I'm assuming that the 'index' is simply the order in which they're yielded from the IEnumerable. That's all I need to correlate with in my case.
Erik Forbes
Btw - upvoting because your concerns are incredibly relevant (if entirely expected and controllable in my case).
Erik Forbes
+6  A: 

I use these extension methods:

// From http://community.bartdesmet.net/blogs/bart/archive/2008/11/03/c-4-0-feature-focus-part-3-intermezzo-linq-s-new-zip-operator.aspx
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> func) {
    if (first == null) 
        throw new ArgumentNullException("first");
    if (second == null) 
        throw new ArgumentNullException("second");
    if (func == null)
        throw new ArgumentNullException("func");
    using (var ie1 = first.GetEnumerator())
    using (var ie2 = second.GetEnumerator())
        while (ie1.MoveNext() && ie2.MoveNext())
            yield return func(ie1.Current, ie2.Current);
}

public static IEnumerable<KeyValuePair<T, R>> Zip<T, R>(this IEnumerable<T> first, IEnumerable<R> second) {
    return first.Zip(second, (f, s) => new KeyValuePair<T, R>(f, s));
}

EDIT: after the comments I'm obliged to clarify and fix some things:

  • I originally took the first Zip implementation verbatim from Bart De Smet's blog
  • Added enumerator disposing (which was also noted on Bart's original post)
  • Added null parameter checking (also discussed in Bart's post)
Mauricio Scheffer
Nicer than mine.
erikkallen
This is wrong as it assumes IEnumerables preserve order.
Welbog
Sort of: it encourages the _caller_ to make the assumption. This does the only thing it can do, and sometimes the assumption is well-founded.
Joel Coehoorn
You should dispose your enumerators, as well.
Reed Copsey
Fair enough. Still, if you're not careful about what types you pass to it, you're going to shoot yourself in the foot.
Welbog
@reed: Check out Bart's original article (http://community.bartdesmet.net/blogs/bart/archive/2008/11/03/c-4-0-feature-focus-part-3-intermezzo-linq-s-new-zip-operator.aspx) it covers disposal and other issues.
Mauricio Scheffer
@reed: thanks, I fixed the disposing issue.
Mauricio Scheffer
Thanks - this is exactly what I'm looking for.
Erik Forbes
A: 

Untested, but should work:

IEnumerable<KeyValuePair<T, U>> Zip<T, U>(IEnumerable<T> t, IEnumerable<U> u) {
    IEnumerator<T> et = t.GetEnumerator();
    IEnumerator<U> eu = u.GetEnumerator();

    for (;;) {
        bool bt = et.MoveNext();
        bool bu = eu.MoveNext();
        if (bt != bu)
            throw new ArgumentException("Different number of elements in t and u");
        if (!bt)
            break;
        yield return new KeyValuePair<T, U>(et.Current, eu.Current);
    }
}
erikkallen
A: 

You could use the Zip methods in MoreLINQ.

Jeff Yates
+1  A: 

Look at nextension:

Currently Implemented Methods

IEnumerable

  • ForEach Performs a specified action on each element of the IEnumerable.
  • Clump Groups items into same size lots.
  • Scan Creates a list by applying a delegate to pairs of items in the IEnumerable.
  • AtLeast Checks there are at least a certain amount of items in the IEnumerable.
  • AtMost Checks there are no more than a certain amount of items in the IEnumerable.
  • Zip Creates a list by combining two other lists into one.
  • Cycle Creates a list by repeating another list.
gimel
A: 

The MSDN has the following Custom Sequence Operators example. And Welbog is right; if you have no index on the underlying data you have no guarantee that the operation does what you exspect.

Daniel Brückner
+1  A: 

I would use something along the lines of -

IEnumerable<KeyValuePair<T,U>> Merge<T,U>(IEnumerable<T> keyCollection, IEnumerable<U> valueCollection)
{
    var keys = keyCollection.GetEnumerator();
    var values = valueCollection.GetEnumerator();
    try
    { 
        keys.Reset();
        values.Reset();

        while (keys.MoveNext() && values.MoveNext())
        {
            yield return new KeyValuePair<T,U>(keys.Current,values.Current);
        }
    }
    finally
    {
        keys.Dispose();
        values.Dispose();
    }
}

This should work correctly, and cleanup properly afterwards.

Reed Copsey
I think it's good form to call it "zip", as that is a know operation in the functional world.
Daniel
A: 

JaredPar has a library with a lot of useful stuff in it, include Zip which will enable what you want to do.

Daniel Earwicker
A: 

Another implementation from the functional-dotnet project by Alexey Romanov:

/// <summary>
/// Takes two sequences and returns a sequence of corresponding pairs. 
/// If one sequence is short, excess elements of the longer sequence are discarded.
/// </summary>
/// <typeparam name="T1">The type of the 1.</typeparam>
/// <typeparam name="T2">The type of the 2.</typeparam>
/// <param name="sequence1">The first sequence.</param>
/// <param name="sequence2">The second sequence.</param>
/// <returns></returns>
public static IEnumerable<Tuple<T1, T2>> Zip<T1, T2>(
 this IEnumerable<T1> sequence1, IEnumerable<T2> sequence2) {
 using (
  IEnumerator<T1> enumerator1 = sequence1.GetEnumerator())
 using (
  IEnumerator<T2> enumerator2 = sequence2.GetEnumerator()) {
  while (enumerator1.MoveNext() && enumerator2.MoveNext()) {
   yield return
    Pair.New(enumerator1.Current, enumerator2.Current);
  }
 }
 //
 //zip :: [a] -> [b] -> [(a,b)]
 //zip (a:as) (b:bs) = (a,b) : zip as bs
 //zip _      _      = []
}

Replace Pair.New with new KeyValuePair<T1, T2> (and the return type) and you're good to go.

Mauricio Scheffer
+1  A: 

Still another from Eric Lippert: link text

n8wrl
Funny - I just read this last night. =)
Erik Forbes
+1  A: 

As a update to anyone stumbling across this question, .Net 4.0 supports this natively as ex from MS:

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);
lasseespeholt