开发者

Split a List into a List of Lists, splitting on an element

Can the following be rewritten so that is uses LINQ, (rather an these old-fashioned foreach loops)

IEnumerable<IEnumerable<T>> SplitIntoSections<T>(IEnumerable<T> content, 
    Func<T, bool> isSectionDivider)
{
    var sections = new List<List<T>>();
    sections.Add(new List<T>());
    foreach (var element in content)
    {
        if (isSectionDivider(element))
        {
            sections.Add(new List<T>());
        }
        else
        {
            sections.Last().Add(element);
        }
    }

    return sections;
}

I thought I almost had an way of doing this, (it involved FSharp colections) when i realised that it could be done w开发者_运维技巧ith a foreach loop.


You don't want to use LINQ here. You aren't going to be able to order and group the proper way without doing something gnarly.

The easy thing to do is to take your code and make it defer execution using the yield statement. An easy way to do this is as follows:

IEnumerable<IEnumerable<T>> SplitIntoSections<T>(this IEnumerable<T> source, 
    Func<T, bool> sectionDivider)
{
    // The items in the current group.
    IList<T> currentGroup = new List<T>();

    // Cycle through the items.
    foreach (T item in source)
    {
        // Check to see if it is a section divider, if
        // it is, then return the previous section.
        // Also, only return if there are items.
        if (sectionDivider(item) && currentGroup.Count > 0)
        {
            // Return the list.
            yield return currentGroup;

            // Reset the list.
            currentGroup = new List<T>();
        }

        // Add the item to the list.
        currentGroup.Add(item);
    }

    // If there are items in the list, yield it.
    if (currentGroup.Count > 0) yield return currentGroup;
}

There's a problem here; for very large groups, it's inefficient to store the sub-groups in a list, they should be streamed out as well. The problem with your approach is that you have a function that is required to be called on each item; it interferes with the stream operation since one can't reset the stream backwards once the grouping is found (as you effectively need two methods that yield results).


Here's an inefficient but pure LINQ solution:

var dividerIndices = content.Select((item, index) => new { Item = item, Index = index })
                            .Where(tuple => isSectionDivider(tuple.Item))
                            .Select(tuple => tuple.Index);


return new[] { -1 }
        .Concat(dividerIndices)
        .Zip(dividerIndices.Concat(new[] { content.Count() }),
            (start, end) => content.Skip(start + 1).Take(end - start - 1));


You could use a side-effect which is only used within a well-defined area... it's pretty smelly, but:

int id = 0;
return content.Select(x => new { Id = isSectionDivider(x) ? id : ++id,
                                 Value = x })
              .GroupBy(pair => pair.Id, pair.Value)
              .ToList();

There must be a better alternative though... Aggregate will get you there if necessary...

return content.Aggregate(new List<List<T>>(), (lists, value) => {
                             if (lists.Count == 0 || isSectionDivider(value)) {
                                 lists.Add(new List<T>());
                             };
                             lists[lists.Count - 1].Add(value);
                             return lists;
                         });

... but overall I agree with casperOne, this is a situation best handled outside LINQ.


Well, I use one LINQ method here, though it's not particularly in the spirit of your question, I think:

static class Utility
{
    // Helper method since Add is void
    static List<T> Plus<T>(this List<T> list, T newElement)
    {
        list.Add(newElement);
        return list;
    }

    // Helper method since Add is void
    static List<List<T>> PlusToLast<T>(this List<List<T>> lists, T newElement)
    {
        lists.Last().Add(newElement);
        return lists;
    }

    static IEnumerable<IEnumerable<T>> SplitIntoSections<T>
         (IEnumerable<T> content, 
          Func<T, bool> isSectionDivider)
    {
        return content.Aggregate(                      // a LINQ method!
            new List<List<T>>(),                       // start with empty sections
            (sectionsSoFar, element) =>
            isSectionDivider(element)
                ? sectionsSoFar.Plus(new List<T>())
                  // create new section when divider encountered

                : sectionsSoFar.PlusToLast(element)
                  // add normal element to current section
            );
    }
}

I trust you will note the complete lack of error checking, should you decide to use this code...

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜