How take each two items from IEnumerable as a pair?
Asked Answered
A

7

6

I have IEnumerable<string> which looks like {"First", "1", "Second", "2", ... }.

I need to iterate through the list and create IEnumerable<Tuple<string, string>> where Tuples will look like:

"First", "1"

"Second", "2"

So I need to create pairs from a list I have to get pairs as mentioned above.

Antilog answered 8/3, 2011 at 23:29 Comment(2)
Can't you just iterate over it?Matsu
I'd like to avoid writing foreach loops and ugly code :P Is there any chance of getting this using IEnumberable methods?Antilog
R
5

You could do something like:

var pairs = source.Select((value, index) => new {Index = index, Value = value})
                  .GroupBy(x => x.Index / 2)
                  .Select(g => new Tuple<string, string>(g.ElementAt(0).Value, 
                                                         g.ElementAt(1).Value));

This will get you an IEnumerable<Tuple<string, string>>. It works by grouping the elements by their odd/even positions and then expanding each group into a Tuple. The benefit of this approach over the Zip approach suggested by BrokenGlass is that it only enumerates the original enumerable once.

It is however hard for someone to understand at first glance, so I would either do it another way (ie. not using linq), or document its intention next to where it is used.

Rumen answered 8/3, 2011 at 23:59 Comment(1)
This is by far the most inefficient method, even if it looks like functional approach. Also, it throws ArgumentOutOfRangeException on incomplete input in case of an odd count of elements in source.Almire
P
14

A lazy extension method to achieve this is:

public static IEnumerable<Tuple<T, T>> Tupelize<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        while (enumerator.MoveNext())
        {
            var item1 = enumerator.Current;

            if (!enumerator.MoveNext())
                throw new ArgumentException();

            var item2 = enumerator.Current;

            yield return new Tuple<T, T>(item1, item2);
        }
}

Note that if the number of elements happens to not be even this will throw. Another way would be to use this extensions method to split the source collection into chunks of 2:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> list, int batchSize)
{

    var batch = new List<T>(batchSize);

    foreach (var item in list)
    {
        batch.Add(item);
        if (batch.Count == batchSize)
        {
            yield return batch;
            batch = new List<T>(batchSize);
        }
    }

    if (batch.Count > 0)
        yield return batch;
}

Then you can do:

var tuples = items.Chunk(2)
    .Select(x => new Tuple<string, string>(x.First(), x.Skip(1).First()))
    .ToArray();

Finally, to use only existing extension methods:

var tuples = items.Where((x, i) => i % 2 == 0)
    .Zip(items.Where((x, i) => i % 2 == 1), 
                     (a, b) => new Tuple<string, string>(a, b))
    .ToArray();
Plus answered 8/3, 2011 at 23:42 Comment(0)
C
5

morelinq contains a Batch extension method which can do what you want:

var str = new string[] { "First", "1", "Second", "2", "Third", "3" };
var tuples = str.Batch(2, r => new Tuple<string, string>(r.FirstOrDefault(), r.LastOrDefault()));
Cosmonautics answered 8/3, 2011 at 23:56 Comment(0)
R
5

You could do something like:

var pairs = source.Select((value, index) => new {Index = index, Value = value})
                  .GroupBy(x => x.Index / 2)
                  .Select(g => new Tuple<string, string>(g.ElementAt(0).Value, 
                                                         g.ElementAt(1).Value));

This will get you an IEnumerable<Tuple<string, string>>. It works by grouping the elements by their odd/even positions and then expanding each group into a Tuple. The benefit of this approach over the Zip approach suggested by BrokenGlass is that it only enumerates the original enumerable once.

It is however hard for someone to understand at first glance, so I would either do it another way (ie. not using linq), or document its intention next to where it is used.

Rumen answered 8/3, 2011 at 23:59 Comment(1)
This is by far the most inefficient method, even if it looks like functional approach. Also, it throws ArgumentOutOfRangeException on incomplete input in case of an odd count of elements in source.Almire
P
4

You can make this work using the LINQ .Zip() extension method:

IEnumerable<string> source = new List<string> { "First", "1", "Second", "2" };
var tupleList = source.Zip(source.Skip(1), 
                           (a, b) => new Tuple<string, string>(a, b))
                      .Where((x, i) => i % 2 == 0)
                      .ToList();

Basically the approach is zipping up the source Enumerable with itself, skipping the first element so the second enumeration is one off - that will give you the pairs ("First, "1"), ("1", "Second"), ("Second", "2").

Then we are filtering the odd tuples since we don't want those and end up with the right tuple pairs ("First, "1"), ("Second", "2") and so on.

Edit:

I actually agree with the sentiment of the comments - this is what I would consider "clever" code - looks smart, but has obvious (and not so obvious) downsides:

  1. Performance: the Enumerable has to be traversed twice - for the same reason it cannot be used on Enumerables that consume their source, i.e. data from network streams.

  2. Maintenance: It's not obvious what the code does - if someone else is tasked to maintain the code there might be trouble ahead, especially given point 1.

Having said that, I'd probably use a good old foreach loop myself given the choice, or with a list as source collection a for loop so I can use the index directly.

Pokeberry answered 8/3, 2011 at 23:44 Comment(3)
@Nebo: Be careful with this approach as it iterates over the source enumerable more than once. This may be a problem if the source enumerable cannot be evaluated more than once or if it contains many items.Rumen
Just a warning: This solution does not work if you only have access to the IEnumerator, and it can consume more resources if making new IEnumerators is expensive. Use with caution if you use it on unconventional data types.Futures
True enough, the performance isn't optimal using this method. I've just came across MoreLinq so I'll check that out too. Thanks for the warnings :)Antilog
A
3

Starting from NET 6.0, you can use Enumerable.Chunk(IEnumerable, Int32)

var tuples = new[] {"First", "1", "Second", "2", "Incomplete" }
    .Chunk(2)
    .Where(chunk => chunk.Length == 2)
    .Select(chunk => (chunk[0], chunk[1]));
Almire answered 3/12, 2022 at 10:52 Comment(0)
F
1
IEnumerable<T> items = ...;
using (var enumerator = items.GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        T first = enumerator.Current;
        bool hasSecond = enumerator.MoveNext();
        Trace.Assert(hasSecond, "Collection must have even number of elements.");
        T second = enumerator.Current;

        var tuple = new Tuple<T, T>(first, second);
        //Now you have the tuple
    }
}
Futures answered 8/3, 2011 at 23:38 Comment(4)
Thanks for the answer, any chance of writing this using LINQ? (just noticed SLaks' comment below saying LINQ can't do it)Antilog
@Nebo: Not that I know of (though there probably is some way), but might I ask "why"? I mean, what's so special about LINQ?Futures
Just wanted to know if there's a way to do it.Antilog
@Nebo: Haha okay... curiosity never hurts I guess. :)Futures
S
0

If you are using .NET 4.0, then you can use tuple object (see http://mutelight.org/articles/finally-tuples-in-c-sharp.html). Together with LINQ it should give you what you need. If not, then you probably need to define your own tuples to do that or encode those strings like for example "First:1", "Second:2" and then decode it (also with LINQ).

Sepalous answered 8/3, 2011 at 23:35 Comment(2)
Yeah, I plan to use .NET 4.0 Tuple objects, but that wasn't my question actually. I need to create Tuples from my IEnumerable<string> for the given rule.Antilog
Of course it CAN. For example, if your initial list is called input, then var output = from ob in input where inout.IndexOf(ob) % 2 == 1 join eb in input on input.IndexOf(ob) equals input.IndexOf(eb) + 1 select new Tuple<string,string>(eb, ob); will do the trick.Sepalous

© 2022 - 2024 — McMap. All rights reserved.