Linq Consecutive GroupBy

pritaeas 0 Tallied Votes 1K Views Share

For a report I had to show a list of items (ID's). Instead of showing them all, they wanted to show consecutive items separated by a dash. Just like the way you would use print pages in Word for example. In the print dialog you can say 1-5, 8 to indicate pages 1, 2, 3, 4, 5, 8. The code shows how you can do this with Linq in a single (but long) line.

I've created the ConsecutiveGroup class for it, because both the standard KeyValuePair and Tuple classes I wanted to use at first have read-only properties.

Feel free to comment or question.

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsecutiveGroupBy
{
    public class ConsecutiveGroup
    {
        public ConsecutiveGroup(int name, int value)
        {
            First = name;
            Last = value;
        }

        public int First { get; set; }
        public int Last { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // test data
            List<int> data = new List<int>()
            {
                1, 8, 3, 5, 7, 7, 7, 7, 7, 2
            };

            string consecutives =
                data                // our list of integers
                .Distinct()         // remove duplicates
                .OrderBy(i => i)    // sort ascending
                .Aggregate(
                    // loop through all items creating a new list
                    new List<ConsecutiveGroup>(), (list, item) =>
                    {
                        if (list.Count == 0 || list.Last().Last + 1 != item)
                        {
                            // the new list is empty, 
                            // or the last property of the last item in the list is not next to this item
                            list.Add(new ConsecutiveGroup(item, item));
                        }
                        else
                        {
                            // the last property of the last item in the list is next to this item,
                            // store it as the new last of this consecutive group
                            list.Last().Last = item;
                        }
                        return list;
                    }) // here we have a list containing (1, 3), (5, 5), and (7, 8)
                    // loop through the new list creating the output string
                    .Aggregate(string.Empty, (str, item) =>
                    {
                        // if the string is not empty add a comma as separator
                        if (!string.IsNullOrEmpty(str))
                        {
                            str += ", ";
                        }

                        // if first equals last, add only first,
                        // else add first-last
                        str += (item.First == item.Last) 
                            ? item.First.ToString()
                            : string.Format("{0}-{1}", item.First, item.Last);
                        return str;
                    }); // here we have "1-3, 5, 7-8"

            Console.WriteLine(consecutives);
            Console.ReadKey();
        }
    }
}
pritaeas 2,194 ¯\_(ツ)_/¯ Moderator Featured Poster

Here's an update. Turned the code into a Linq extension to make it more flexible:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsecutiveGroupBy
{
    public static class Extensions
    {
        private class ConsecutiveGroup<T>
        {
            public T First { get; set; }
            public T Last { get; set; }
        }

        public static string ConsecutiveGroupBy<T, TResult>(
            this IEnumerable<T> instance, 
            Func<T, TResult> selector,
            Func<TResult, TResult, bool> isAdjacentFunc, 
            string combinator, string separator) 
        {
            string result = string.Empty;

            if (instance != null)
            {
                result =
                    instance
                    .OrderBy(selector)
                    .Aggregate(
                        new List<ConsecutiveGroup<T>>(), (list, item) =>
                        {
                            if (list.Count == 0 || 
                                (!Equals(selector.Invoke(list.Last().Last), selector.Invoke(item)) && 
                                    !isAdjacentFunc(selector.Invoke(list.Last().Last), selector.Invoke(item))))
                            {
                                list.Add(new ConsecutiveGroup<T> { First = item, Last = item });
                            }
                            else
                            {
                                list.Last().Last = item;
                            }
                            return list;
                        })
                        .Aggregate(string.Empty, (str, item) =>
                        {
                            if (!string.IsNullOrEmpty(str))
                            {
                                str += separator;
                            }

                            str += Equals(selector.Invoke(item.First), selector.Invoke(item.Last))
                                ? (selector.Invoke(item.First)).ToString()
                                : string.Format("{0}{1}{2}", selector.Invoke(item.First), combinator, selector.Invoke(item.Last));

                            return str;
                        });
            }

            return result;
        }
    }

    public class TestClass
    {
        public int Key { get; set; }
        public int Value { get; set; }
        public char Char { get; set; }
    }

    class Program
    {
        static void Main()
        {
            List<TestClass> data = new List<TestClass>
            {
                new TestClass { Key = 8, Value = 1, Char = 'A' }, 
                new TestClass { Key = 7, Value = 8, Char = 'B' }, 
                new TestClass { Key = 6, Value = 3, Char = 'H' }, 
                new TestClass { Key = 5, Value = 5, Char = 'D' }, 
                new TestClass { Key = 4, Value = 7, Char = 'e' }, 
                new TestClass { Key = 3, Value = 7, Char = 'C' }, 
                new TestClass { Key = 1, Value = 7, Char = 'G' }, 
                new TestClass { Key = 1, Value = 2, Char = 'F' }
            };

            string consecutiveKeys = data.ConsecutiveGroupBy(item => item.Key, (current, next) => current + 1 == next, "-", ",");
            string consecutiveValues = data.ConsecutiveGroupBy(item => item.Value, (current, next) => current + 1 == next, " t/m ", ", ");
            string consecutiveChars = data.ConsecutiveGroupBy(item => item.Char, (current, next) => current == next - 1, " - ", ", ");

            Console.WriteLine("By Key   : {0}", consecutiveKeys);
            Console.WriteLine("By Value : {0}", consecutiveValues);
            Console.WriteLine("By Char  : {0}", consecutiveChars);
            Console.ReadKey();
        }
    }
}

Open to suggestions.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.