4

Given a collection, I need to iterate through all the elements three (or some other amount) at a time. For example:

string[] exampleData = {"John", "Doe", "1.1.1990", "Jane", "Roe", "2.2.1980"}

for(int i = 0; i < exampleData.Length; i += 3) {
    CreateUser(foreName: exampleData[i], surName: exampleData[i+1], dateOfBirth: exampleData[i+2]);
} 

How could I efficiently reproduce this if exampleData was an IEnumerable instead of an array?

dcastro
  • 66,540
  • 21
  • 145
  • 155
Dan
  • 51
  • 1
  • 3
  • 1
    possible duplicate of [How do I select every 6th element from a list (using Linq)](http://stackoverflow.com/questions/2453799/how-do-i-select-every-6th-element-from-a-list-using-linq) – Asik Nov 17 '13 at 21:18
  • You can normalize on List by calling .ToList() before iterating – TGH Nov 17 '13 at 21:20
  • 4
    While it would be possible to arrange such an iteration, this would be solving the wrong problem. The right thing to do would be to get your data out of an array and into a collection of an appropriate `Person` type. – Jon Nov 17 '13 at 21:20
  • @Jon I agree. The best approach is to structure the data to meet the requirements better. – TGH Nov 17 '13 at 21:22
  • IMO your code is better than anything you can build with a built in LINQ functions. – CodesInChaos Nov 17 '13 at 21:25
  • @Jon: lol, that might be exactly what he is trying to do with the code he gave. `CreateUser` probably returns the object you are referring to, the assignment is just not shown in the example to make it simpler. The question is how to achieve that if you don't have an array as in input, but rather IEnumerable. (And the answer is to do .ToList() or .ToArray() as mentioned above) – Andrew Savinykh Nov 17 '13 at 21:32
  • @zepri Correct, I have no control over the actual collection I am given. This is a simplified example for the sake of clarity. – Dan Nov 17 '13 at 22:04
  • possible duplicate of [How can I get every nth item from a List?](http://stackoverflow.com/questions/682615/how-can-i-get-every-nth-item-from-a-listt) – Ryan Gates Apr 16 '14 at 19:14

6 Answers6

10

.NET 6 and later

You can use the LINQ Chunk extension method:

foreach (string[] chunk in exampleData.Chunk(3))
{
    CreateUser(foreName: chunk[0], surName: chunk[1], dateOfBirth: chunk[2]);
}

.NET 5 and earlier

You can write your own extension method:

public static IEnumerable<IList<T>> ChunksOf<T>(this IEnumerable<T> sequence, int size)
{
    List<T> chunk = new List<T>(size);

    foreach (T element in sequence)
    {
        chunk.Add(element);
        if (chunk.Count == size)
        {
            yield return chunk;
            chunk = new List<T>(size);
        }
    }
}

Call it like this:

foreach (IList<string> chunk in exampleData.ChunksOf(3))
{
    CreateUser(foreName: chunk[0], surName: chunk[1], dateOfBirth: chunk[2]);
}

Note that if sequence.Count() is not an integer multiple of size, then ChunksOf discards the last partial chunk. If instead you wanted to return a partial chunk, you could add to the end: if (chunk.Count > 0) yield return chunk;.

Michael Liu
  • 52,147
  • 13
  • 117
  • 150
  • This seems like a very elegant answer, thanks. Is there any meaningful overhead compared to simply using ToList() as suggested in the comments above? – Dan Nov 17 '13 at 22:07
  • 1
    @Dan: I don't think there's any significant overhead. In fact, if you're streaming from a very large file (e.g., with `File.ReadLines`) or other data source, then `ChunksOf` will use less memory than calling `ToList` or `GroupBy` (as in dcastro's answer) because both `ToList` and `GroupBy` will read the whole file into memory, whereas `ChunksOf` only keeps one small chunk in memory at a time. – Michael Liu Nov 18 '13 at 00:50
  • `Chunk()` comes with Linq as of this comment date. – Code-Apprentice Apr 04 '23 at 07:20
4

This snippet groups your array into sets of 3 elements, and then creates a user for each set.

    exampleData.Select((elem, index) => new {elem, index})
        .GroupBy(x => x.index/3)
        .Select(x => CreateUser(
                          x.ElementAt(0).elem,
                          x.ElementAt(1).elem,
                          x.ElementAt(2).elem));

Naturally, you'll need to make sure the array's length is a multiple of 3.

dcastro
  • 66,540
  • 21
  • 145
  • 155
1

How about forgetting foreach, the fancy linq stuff, temporary sublists, etc., and going back to the roots? :-)

Call .GetEnumerator() and call .MoveNext() three times in an iteration. That's what enumeration is at its core.

The previous answers seem overly complicated to me. This is simple and hardcore :-).

fejesjoco
  • 11,763
  • 3
  • 35
  • 65
1

Although I prefer Michael's approach, you may prefer something a little more specific if you need field specific parsing (eg DOB parsing)

public static IEnumerable<User> ExtractUsers(IEnumerable<string> input)
{
    int i = 0;
    string forename;
    string surname;
    string dateofbirth;
    foreach (string current in input) 
    {
        switch (i) 
        {
        case 0:
            one = forename;
            break;
        case 1:
            two = surname;
            break;
        case 2:
            three = dateofbirth;
            i = -1; //uck, this will get incremented at the bottom
            yield return CreateUser(forename, surname, dateofbirth);
            break;
        }
        i++;
    }
}
jaquesri
  • 188
  • 2
0

Take() and Skip() will do what you want, but I would advise you to avoid using arrays like this. You'd be better off creating a Person class with FirstName, LastName, and DateOfBirth properties and keeping an array of those.

0

You can use LINQ

        string[] exampleData = {"John", "Doe", "1.1.1990", "Jane", "Roe", "2.2.1980"};

        int chunkSize = 3;
        var seqences = from seqIndex in Enumerable.Range(0, exampleData.Length)
                       group exampleData[seqIndex] by seqIndex / chunkSize;
        foreach (var sub in seqences)
        {
            CreateUser(sub.ToList()[0], sub.ToList()[1], sub.ToList()[2]);
        }

Edit:

     var seqences = from m in Enumerable.Range(0, exampleData.Count()) group 
                           exampleData.ToList()[m] by m / chunkSize;

or

        int chunkSize = 3;
        var seqences = Enumerable.Range(0, exampleData.Count())
                       .GroupBy(m => m/chunkSize, m => exampleData.ToList()[m]);
Alyafey
  • 1,455
  • 3
  • 15
  • 23