Как найти последовательные элементы с такими же значениями, как группа Linq

var schedules = new List{ new Item { Id=1, Name = "S" }, new Item { Id=2, Name = "P" }, new Item { Id=3, Name = "X" }, new Item { Id=4, Name = "X" }, new Item { Id=5, Name = "P" }, new Item { Id=6, Name = "P" }, new Item { Id=7, Name = "P" }, new Item { Id=8, Name = "S" } }; 

Я хочу выбрать одинаковые значения и одинаковые заказы в новом списке:

 var groupedAndSelectedList = new List<List>{ new List { new Item { Id=3, Name = "X" }, new Item { Id=4, Name = "X" }, }, new List { new Item { Id=5, Name = "P" }, new Item { Id=6, Name = "P" }, new Item { Id=7, Name = "P" }, } } 

Если элемент одинаков, как new Item { Id=3, Name = "A" } Мне не нужно его получать.

Группа, выбирает все элементы X или P в списке. Но я хочу, чтобы пункты стояли после или перед другим элементом.

Возможно ли это с помощью linq?

Здесь вы ищете метод GroupWhile .

Кредит для пользователя LB для решения. Идите, дайте свой первоначальный ответ Updoot https://stackoverflow.com/a/20469961/30155

  var schedules = new List{ new Item { Id=1, Name = "S" }, new Item { Id=2, Name = "P" }, new Item { Id=3, Name = "X" }, new Item { Id=4, Name = "X" }, new Item { Id=5, Name = "P" }, new Item { Id=6, Name = "P" }, new Item { Id=7, Name = "P" }, new Item { Id=8, Name = "S" } }; var results = schedules .GroupWhile((preceding, next) => preceding.Name == next.Name) //Group items, while the next is equal to the preceding one .Where(s => s.Count() > 1) //Only include results where the generated sublist have more than 1 element. .ToList(); foreach (var sublist in results) { foreach (Item i in sublist) { Console.WriteLine($"{i.Name} - {i.Id}"); } Console.WriteLine(""); } Console.ReadLine(); 

Вы можете добавить реализацию как метод расширения для всех IEnumerable как это.

 public static class Extensions { public static IEnumerable> GroupWhile(this IEnumerable seq, Func condition) { T prev = seq.First(); List list = new List() { prev }; foreach (T item in seq.Skip(1)) { if (condition(prev, item) == false) { yield return list; list = new List(); } list.Add(item); prev = item; } yield return list; } } 

Вы можете сделать это, сохранив количество элементов, которые вы нашли до сих пор. Это поможет вам найти последовательные элементы, потому что значение count(name) - index для них инвариантно:

 IDictionary count = new Dictionary(); var groups = schedules .Select((s, i) => new { Item = s , Index = i }) .GroupBy(p => { var name = p.Item.Name; int current; if (!count.TryGetValue(name, out current)) { current = 0; count.Add(name, current); } count[name] = current + 1; return new { Name = name, Order = current - p.Index }; }) .Select(g => g.ToList()) .Where(g => g.Count > 1) .ToList(); 

Это дает желаемый результат для вашего примера:

 { Item = Id=3 Name=X, Index = 2 } { Item = Id=4 Name=X, Index = 3 } ----- { Item = Id=5 Name=P, Index = 4 } { Item = Id=6 Name=P, Index = 5 } { Item = Id=7 Name=P, Index = 6 } 

Demo.

Примечание. Если выражение Order = current - p.Index выглядит немного как «магия», рассмотрите удаление окончательных предложений Select и Where и перечисление групповых клавиш.

@dasblinkenlight предоставил ответ, который просто использует LINQ. Любой ответ с использованием только существующих методов LINQ может быть уродливым, может плохо выполняться и не может быть многократно использованным повторно. (Это не критика этого ответа. Это критика LINQ.)

@ eoin-campbell предоставил ответ, который использует настраиваемый метод LINQ. Однако я думаю, что его можно улучшить, чтобы более точно сопоставлять возможности существующей функции LINQ GroupBy , такие как пользовательские сопоставления (когда вам нужно делать такие вещи, как нечувствительное к регистру сравнение ключей). Этот метод Partition ниже выглядит и чувствует себя как функция GroupBy но отвечает требованиям для последовательных элементов.

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

 var partitionsWithMoreThan1 = schedules.Partition(o => o.Name) .Where(p => p.Count() > 1) .Select(p => p.ToList()) .ToList(); 

Вот способ:

 static class EnumerableExtensions { ///  /// Partitions the elements of a sequence into smaller collections according to a specified /// key selector function, optionally comparing the keys by using a specified comparer. /// Unlike GroupBy, this method does not produce a single collection for each key value. /// Instead, this method produces a collection for each consecutive set of matching keys. ///  /// The type of the elements of . /// The type of the key returned by . /// An  whose elements to partition. /// A function to extract the key for each element. /// An  to compare keys. ///  /// An IEnumerable{IGrouping{TKey, TSource}} in C# /// or IEnumerable(Of IGrouping(Of TKey, TSource)) in Visual Basic /// where each  object contains a collection of objects and a key. ///  public static IEnumerable> Partition(this IEnumerable source, Func keySelector, IEqualityComparer comparer = null) { if (comparer == null) comparer = EqualityComparer.Default; using (var enumerator = source.GetEnumerator()) { if (enumerator.MoveNext()) { var item = enumerator.Current; var partitionKey = keySelector(item); var itemsInPartition = new List {item}; var lastPartitionKey = partitionKey; while (enumerator.MoveNext()) { item = enumerator.Current; partitionKey = keySelector(item); if (comparer.Equals(partitionKey, lastPartitionKey)) { itemsInPartition.Add(item); } else { yield return new Grouping(lastPartitionKey, itemsInPartition); itemsInPartition = new List {item}; lastPartitionKey = partitionKey; } } yield return new Grouping(lastPartitionKey, itemsInPartition); } } } // it's a shame there's no ready-made public implementation that will do this private class Grouping : IGrouping { public Grouping(TKey key, List items) { _items = items; Key = key; } public TKey Key { get; } public IEnumerator GetEnumerator() { return _items.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _items.GetEnumerator(); } private readonly List _items; } } 

Основываясь на пояснениях комментариев (сейчас вопрос непонятен), я думаю, что это то, что нужно.

Он использует метод расширения, который группирует группы ключей вместе, GroupByRuns , который основан на GroupByWhile путем тестирования последовательных элементов, который основан на ScanPair , который является вариацией моего оператора Scan APL, который похож на Aggregate , но возвращает промежуточные результаты и использует ValueTuple (Key, Value) для сопряжения ключей со значениями на этом пути.

 public static IEnumerable> GroupByRuns(this IEnumerable src, Func keySelector, Func resultSelector, IEqualityComparer cmp = null) { cmp = cmp ?? EqualityComparer.Default; return src.GroupByWhile((prev, cur) => cmp.Equals(keySelector(prev), keySelector(cur)), resultSelector); } public static IEnumerable> GroupByRuns(this IEnumerable src, Func keySelector) => src.GroupByRuns(keySelector, e => e); public static IEnumerable> GroupByRuns(this IEnumerable src) => src.GroupByRuns(e => e, e => e); public static IEnumerable> GroupByWhile(this IEnumerable src, Func testFn, Func resultFn) => src.ScanPair(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1) .GroupBy(kvp => kvp.Key, kvp => resultFn(kvp.Value)); public static IEnumerable<(TKey Key, T Value)> ScanPair(this IEnumerable src, TKey seedKey, Func<(TKey Key, T Value),T,TKey> combineFn) { using (var srce = src.GetEnumerator()) { if (srce.MoveNext()) { var prevkv = (seedKey, srce.Current); while (srce.MoveNext()) { yield return prevkv; prevkv = (combineFn(prevkv, srce.Current), srce.Current); } yield return prevkv; } } } 

Я понимаю, что это много кода расширения, но, используя общую базу ScanPair , вы можете создавать другие специализированные методы группировки, такие как GroupBySequential .

Теперь вы просто GroupByRuns of Name и выбираете прогоны с более чем одним членом, а затем конвертируете каждый прогон в List и все это в List :

 var ans = schedules.GroupByRuns(s => s.Name) .Where(sg => sg.Count() > 1) .Select(sg => sg.ToList()) .ToList(); 

ПРИМЕЧАНИЕ. Для @ Aominè, у которого было интересное занятие по оптимизации Count() > 1 используя Take(2).Count() или @MichaelGunter, используя Skip(1).Any() , после GroupBy подгруппы (внутренний тип Grouping ) каждый реализует IList и метод Count() просто получает счет непосредственно из поля Grouping.count .