Search code examples
c#linq

Group objects by property in C#, maintaining original order


I have a collection of objects in C# of type IEnumerable<BlockListItem>. Each block list item has a string property called 'Alias'. I need to be able to group the items in sequence by their Alias. However, certain aliases should not be allowed to be grouped with others, they should be the only item in the group. The non-groupable aliases are defined as a list of strings.

Here's a written example of the original data:

Item 1 (alias A)
Item 2 (alias A)
Item 3 (alias B)
Item 4 (alias C)
Item 5 (alias C)
Item 6 (alias A)
Item 7 (alias A)
...etc

Assume that anything with the alias of C should be ungroupable.

What I want the outcome to be is:

Group 1
  Item 1 (alias A)
  Item 2 (alias A)
Group 2
  Item 3 (alias B)
Group 3
  Item 4 (alias C)
Group 4
  Item 5 (alias C)
Group 5
  Item 6 (alias A)
  Item 7 (alias A)

In need to preserve the order of the items, as per the outcome above.

Could anyone please suggest a neat way to accomplish this. I've tried Linq Grouping but that doesn't preserve the order; this custom GroupWhile extension looks interesting but doesn't handle the ungroupable items and my other attempts using loops are way off.


Solution

  • As has been suggested in the comments, I find that a standard loop could be handy for this scenario.

    You could create a List<List<BlockListItem>> variable which you populate with your groups, and a List<BlockListItem> variable to keep track of the current group.

    If implemented in a method, it could look like:

    private static List<List<BlockListItem>> GroupItems(
        IEnumerable<BlockListItem> items,
        string[] ungroupableAliases)
    {
        if (items == null || !items.Any())
        {
            return new List<List<BlockListItem>>();
        }
    
        var itemGroups = new List<List<BlockListItem>>();
        var currentItemGroup = new List<BlockListItem> { items.First() };
    
        var currentItemGroupAlias = items.First().Alias;
        
        foreach (var item in items.Skip(1))
        {
            var itemIsGroupable =
                item.Alias == currentItemGroupAlias && !ungroupableAliases.Contains(item.Alias);
    
            if (!itemIsGroupable)
            {
                itemGroups.Add(currentItemGroup.ToList());
                currentItemGroup.Clear();
    
                currentItemGroupAlias = item.Alias;
            }
            
            currentItemGroup.Add(item);
        }
        
        itemGroups.Add(currentItemGroup.ToList());
        
        return itemGroups;
    }
    

    and be used as follows:

    var ungroupableAliases = new[] { "C" };
    
    IEnumerable<BlockListItem> items = new List<BlockListItem>
    {
        new() { Id = 1, Alias = "A" },
        new() { Id = 2, Alias = "A" },
        new() { Id = 3, Alias = "B" },
        new() { Id = 4, Alias = "C" },
        new() { Id = 5, Alias = "C" },
        new() { Id = 6, Alias = "A" },
        new() { Id = 7, Alias = "A" },
    };
    
    var groupedItems = GroupItems(items, ungroupableAliases);
    

    Depending on how specific the groups need to be designed (e.g. including group number, which is not covered in this approach), you may want to customize the implementation.

    Here is a fiddle to try it out.