LINQ: Добавляем возможность группировки следующих подряд элементов

Всем нам знаком стандартный LINQ-метод GroupBy(). Он позволяет на основе некоего селектора разложить элементы в последовательности по группам. К сожалению, мы не можем в селекторе указать такое нетривиальное условие, как нахождение элементов рядом. А именно это мне и нужно было. Поиск по StackOverflow не дал каких-либо приемлемых решений, и я, скрепя душой, сел изобретать свой велосипед. Ниже представляю код метода аналогичного GroupBy() с одним отличием: группировка происходит только для рядом стоящих элементов.

К примеру, есть последовательность:
1 1 1 2 2 2 2 1 1 1 1 4 4 4 4 2 2 2

После группировки хотим получить следующие группы (в каждой строке по группе):
1 1 1
2 2 2 2
1 1 1 1
4 4 4 4
2 2 2

Для начала возьмем стандартную сигнатуру GroupBy(), чтобы не сильно отличаться, и переименуем ее в GroupContinuously():

IEnumerable<IGrouping<TKey, TSource>> GroupContinuously<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IEqualityComparer<TKey> comparer = null)

Первым параметром идет искомая последовательность, вторым – селектор, третьим – опциональный компаратор.

Как видно, группы здесь представляются интерфейсом IGrouping. Я слегка удивился не найдя стандартных реализаций этого интерфейса в библиотеках фреймворка. Ну ничего, если уж велосипедить, то по полной. Интерфейс всего лишь предоставляет доступ к ключу группы (то по чему группирует селектор) и энумератору для получения сгруппированной последовательности. Создаем приватный класс с нужной реализацией:

static class GroupingExtensions
{
    private class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
    {
        public List<TElement> Elements { get; private set; }
        public TKey Key { get; private set; }

        public Grouping(TKey key)
        {
            Key = key;
            Elements = new List<TElement>();
        }

        public IEnumerator<TElement> GetEnumerator()
        {
            return Elements.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Сама реализация помещена в статический класс GroupingExtensions, куда позже добавим наш новый метод расширения. В самом классе Grouping я намеренно оставил открытый доступ к свойству Elements для упрощения дальнейшей работы с классом. Не будь этот класс приватным, имело бы смысл передавать элементы единожды через конструктор, чтобы не портить иммутабельность (не изменчивость) объекта, но в нашей локальной области видимости это не имеет значения. За пределы класса GroupingExtensions класс Grouping попадет, разве что, в виде интерфейса IGrouping.

Остался основной штрих – сам метод.

public static IEnumerable<IGrouping<TKey, TSource>> GroupContinuously<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IEqualityComparer<TKey> comparer = null)
{
    if (comparer == null) comparer = EqualityComparer<TKey>.Default;
    Grouping<TKey, TSource> group = null;
    foreach (TSource element in source)
    {
        TKey key = keySelector(element);
        if (group == null) group = new Grouping<TKey, TSource>(key);
        if (!comparer.Equals(key, group.Key))
        {
            yield return group;
            group = new Grouping<TKey, TSource>(key);
        }
        group.Elements.Add(element);
    }
    if (group != null) yield return group;
}

Реализация тут довольно проста. Перебираем коллекцию, выбираем из каждого элемента селектором ключ, сравниваем ключи рядом стоящих элементов и, если они совпадают, складываем в одну группу. В случае, если не передан компаратор для ключей, берем компаратор по умолчанию.

Итоговый вариант вместе с классом Grouping:

static class GroupingExtensions
{
    public static IEnumerable<IGrouping<TKey, TSource>> GroupContinuously<TSource, TKey>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector, 
        IEqualityComparer<TKey> comparer = null)
    {
        if (comparer == null) comparer = EqualityComparer<TKey>.Default;
        Grouping<TKey, TSource> group = null;
        foreach (TSource element in source)
        {
            TKey key = keySelector(element);
            if (group == null) group = new Grouping<TKey, TSource>(key);
            if (!comparer.Equals(key, group.Key))
            {
                yield return group;
                group = new Grouping<TKey, TSource>(key);
            }
            group.Elements.Add(element);
        }
        if (group != null) yield return group;
    }

    private class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
    {
        public List<TElement> Elements { get; private set; }
        public TKey Key { get; private set; }

        public Grouping(TKey key)
        {
            Key = key;
            Elements = new List<TElement>();
        }

        public IEnumerator<TElement> GetEnumerator()
        {
            return Elements.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Использование:

IEnumerable<IGrouping<int, int>> grouped = numbers.GroupContinuously(x => x);

Update:
Как оказалось, в библиотеке MoreLinq все же есть аналогичный метод под названием GroupAdjacent().

LINQ: Добавляем возможность группировки следующих подряд элементов: 2 комментария

  1. АватарНурбек

    Добрый вечер!

    Увидел ваш блог, посидев на хабре)

    Мне поставили задачу настроить стенд, на котором стоит винда 10, чтобы была возможность пользоваться только одним сайтом и не было перехода на другие. Также чтобы не было возможности выйти из браузера. Киоск от винды в принципе подходит, но там можно перейти на сторонние сайты через нужный сайт и еще нет возможности вводить свои данные.

    Можете помочь?)

    Заранее благодарю

    Телеграм для связи – Interessanten

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *