Всем нам знаком стандартный 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().
Добрый вечер!
Увидел ваш блог, посидев на хабре)
Мне поставили задачу настроить стенд, на котором стоит винда 10, чтобы была возможность пользоваться только одним сайтом и не было перехода на другие. Также чтобы не было возможности выйти из браузера. Киоск от винды в принципе подходит, но там можно перейти на сторонние сайты через нужный сайт и еще нет возможности вводить свои данные.
Можете помочь?)
Заранее благодарю
Телеграм для связи – Interessanten
Молодец, ты сделал https://github.com/morelinq/MoreLINQ/blob/master/MoreLinq/Segment.cs 😉