Friday, 22 August 2008

Anyone for a Slice of LINQ?

Over on his blog, Eric White has a post about how to chunk a collection into groups of three using standard LINQ operators. As it happens, I've been thinking about the same issue as part of a solution to an upcoming Project Euler problem. Rather than use standard LINQ operators though, I've created a new method called Slice to do the job using a C# iterator.

Give Slice() a sequence, tell it how many items you want to be in each slice or chunk (pick your synonym - might depend on whether you prefer cake or chocolate!), and it will give you back a sequence consisting of slices of the original sequence (each slice is returned as a sequence - an IEnumerable). If the original sequence cannot be evenly divided into slices of exactly the size you require, then the last slice you get back will contain the leftover items (the crumbs, as I've called them in the code below - I clearly prefer cake!).

To adapt Eric's example to the gastronomic analogy that has emerged as I've been writing, consider this example:

public void SliceCakes()
{
    string[] cakeData =
        {
            "Mini Rolls Selection",
            "Cabury",
            "2.39",
            "6 Chocolate Flavour Cup Cakes",
            "Fabulous Bakin' Boys",
            "1.25",
            "Galaxy Cake Bars 5 Pack",
            "McVities",
            "1.00",
            "Apple Slices 6pk",
            "Mr Kipling",
            "1.39"
        };

    var cakes = cakeData
        .Slice(3)
        .Select(slice => new
                 {
                     Cake = slice.ElementAt(0),
                     Baker = slice.ElementAt(1),
                     Price = slice.ElementAt(2)
                 });

    foreach (var cake in cakes)
    {
        Console.WriteLine(cake);
    }
}

This produces the output:

{ Cake = Mini Rolls Selection, Baker = Cadbury, Price = 2.39 }
{ Cake = 6 Chocolate Flavour Cup Cakes, Baker = Fabulous Bakin' Boys, Price = 1.25 }
{ Cake = Galaxy Cake Bars 5 Pack, Baker = McVities, Price = 1.00 }
{ Cake = Apple Slices 6pk, Baker = Mr Kipling, Price = 1.39 }

Here's the code for Slice()

public static class Functional
{
    /// <summary>
    /// Slices a sequence into a sub-sequences each containing maxItemsPerSlice, except for the last
    /// which will contain any items left over
    /// </summary>
    public static IEnumerable<IEnumerable<T>> Slice<T>(this IEnumerable<T> sequence, int maxItemsPerSlice)
    {
        if (maxItemsPerSlice <= 0)
        {
            throw new ArgumentOutOfRangeException("maxItemsPerSlice", "maxItemsPerSlice must be greater than 0");
        }

        List<T> slice = new List<T>(maxItemsPerSlice);

        foreach (var item in sequence)
        {
            slice.Add(item);

            if (slice.Count == maxItemsPerSlice)
            {
                yield return slice.ToArray();
                slice.Clear();
            }
        }

        // return the "crumbs" that 
        // didn't make it into a full slice
        if (slice.Count > 0)
        {
            yield return slice.ToArray();
        }
    }
}

6 comments:

Anonymous said...

Cool, evidently there's a magic spell-checker in linq as well. The string array has it as 'Cabury' but it comes in the output as 'Cadbury'... amazing! ;-)

Sam said...

Anonymous,
The magic is in an invisible call to a WithSpellingCorrected() extension method ;-)

Sam

Anonymous said...

Here is a shoter version

for (IEnumerable<T> header = sequence.Take(maxItemsPerSlice), tail = sequence.Skip(maxItemsPerSlice);
header.Count() == maxItemsPerSlice;
header=tail.Take( maxItemsPerSlice), tail = tail.Skip(maxItemsPerSlice))
{
yield return header;
}

Anonymous said...

shoter version does not yield 'crumb'.

An Phu said...

> gastronomic analogy
I prefer using a metaphor of "confectionary delights".

Anonymous' for loop is pretty creative. Thanks for share.

Richard Birkby said...

            return items.Select((val, i) => new {Value = val, Index = i})
                .GroupBy(item => item.Index/bucketSize)
                .Select(item => item.Select(val => val.Value));

Simples

Post a Comment