Interesting LINQ Exercise
A co-worker posed an interesting LINQ problem to me tonight so I figured I'd share. They had a collection of items and wanted an algorithm that would create a "collection of collections" where the first three items would be grouped together, second three items, on so on. For example, given a sequence like this: { "a", "b", "c", "d", "e", "f", "g", "h" }, it would create a structure that contained 3 groups – the first element would be { "a", "b", "c" }, the second would be { "d", "e", "f" } and the third { "g", "h" }. They already had an algorithm working fine but it was using a "brute force" approach and took about 15-20 lines of code. It "felt like" you could solve the same problem more elegantly with a LINQ solution and with less lines of code.
Here was my first solution:
var list = new List<string> { "a", "b", "c", "d", "e", "f", "g", "h" }; var results = new List<IEnumerable<string>>(); int numBlocks = list.Count % 3 == 0 ? list.Count / 3 : (list.Count / 3) + 1; for (int i = 0; i < numBlocks; i++) { results.Add(list.Skip(i * 3).Take(3)); }
First, I have to figure out the number of "blocks" or elements that the structure (the collection of collection) should have. Next, for each "block position" I leverage the LINQ Skip method in conjunction with the Take method (essentially like a paging operation). I was reasonably happy with the first solution but I kept feeling like I could do it with less lines of code and without the "for" loop. The problem with eliminating the for loop is that it was providing the outer loop and with LINQ you basically have to loop over something. After a little digging, I came across the Enumerable Range method which I hadn't used before. This allowed me to solve the problem with 1 line of code:
var list = new List<string> { "a", "b", "c", "d", "e", "f", "g", "h" }; int numBlocks = list.Count % 3 == 0 ? list.Count / 3 : (list.Count / 3) + 1; var results = Enumerable.Range(0, numBlocks).Select(i => list.Skip(i * 3).Take(3)).ToList();
Now we've got our collection of collection and can iterate over it:
foreach (var item in results) { foreach (var innerItem in item) { Console.WriteLine(innerItem); } }
While I was originally happy that I found a way to do it with 1 line of code, it's not really a great solution because it's just a little "too clever". The for loop is going to be easier to understand for the poor developer that comes behind you and has to maintain your code. Also, the second solution is not as efficient because the Range method creates an array just so i can loop over it just so it can make it workable in LINQ. Bottom line, the first solution is better. But of course, if someone has an even better solution, please do not hesitate to share it in the comment section below.
To take it one step further, the LINQ SelectMany method will allow you to take the collection and collections and flatten it back out to the original single collection like this:
var flattened = results.SelectMany(x => x);