How to find consecutive same values items as a Linq group

前端 未结 4 1107
無奈伤痛
無奈伤痛 2021-01-29 09:23
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,         


        
相关标签:
4条回答
  • 2021-01-29 09:56

    You can do it by maintaining the count of items that you found so far. This helps you find consecutive items, because the value of count(name) - index is invariant for them:

    IDictionary<string,int> count = new Dictionary<string,int>();
    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();
    

    This produces the desired output for your example:

    { 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.

    Note: If Order = current - p.Index expression looks a little like "magic", consider removing the final Select and Where clauses, and enumerating group keys.

    0 讨论(0)
  • 2021-01-29 10:04

    What you're looking for here is a GroupWhile<T> method.

    Credit to user L.B for the solution. Go give his original answer an UpDoot https://stackoverflow.com/a/20469961/30155

        var schedules = new List<Item>{
            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();
    

    You can add the implementation as an Extension Method to all IEnumerable<T> like so.

    public static class Extensions
    {
        public static IEnumerable<IEnumerable<T>> GroupWhile<T>(this IEnumerable<T> seq, Func<T, T, bool> condition)
        {
            T prev = seq.First();
            List<T> list = new List<T>() { prev };
    
            foreach (T item in seq.Skip(1))
            {
                if (condition(prev, item) == false)
                {
                    yield return list;
                    list = new List<T>();
                }
                list.Add(item);
                prev = item;
            }
    
            yield return list;
        }
    }
    
    0 讨论(0)
  • 2021-01-29 10:06

    @dasblinkenlight has provided an answer that just uses LINQ. Any answer using purely existing LINQ methods may be ugly, may perform poorly, and may not be highly reusable. (This is not a criticism of that answer. It's a criticism of LINQ.)

    @eoin-campbell has provided an answer that uses a custom LINQ method. However, I think it can be improved upon to more closely match the capabilities of the existing LINQ GroupBy function, such as custom comparers (for when you need to do things like case-insensitive comparison of the keys). This Partition method below looks and feels like the GroupBy function but meets the requirement for consecutive items.

    You can use this method to meet your goal by doing the following. Notice that it looks exactly like how you would write this if you didn't have the consecutivity requirement, but it's using Partition instead of GroupBy.

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

    Here's the method:

    static class EnumerableExtensions
    {
        /// <summary>
        /// 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.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
        /// <param name="source">An <see cref="IEnumerable{T}"/> whose elements to partition.</param>
        /// <param name="keySelector">A function to extract the key for each element.</param>
        /// <param name="comparer">An <see cref="IEqualityComparer{T}"/> to compare keys.</param>
        /// <returns>
        /// An <b>IEnumerable{IGrouping{TKey, TSource}}</b> in C#
        /// or <b>IEnumerable(Of IGrouping(Of TKey, TSource))</b> in Visual Basic
        /// where each <see cref="IGrouping{TKey,TElement}"/> object contains a collection of objects and a key.
        /// </returns>
        public static IEnumerable<IGrouping<TKey, TSource>> Partition<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null)
        {
            if (comparer == null)
                comparer = EqualityComparer<TKey>.Default;
            using (var enumerator = source.GetEnumerator())
            {
                if (enumerator.MoveNext())
                {
                    var item = enumerator.Current;
                    var partitionKey = keySelector(item);
                    var itemsInPartition = new List<TSource> {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<TKey, TSource>(lastPartitionKey, itemsInPartition);
                            itemsInPartition = new List<TSource> {item};
                            lastPartitionKey = partitionKey;
                        }
                    }
                    yield return new Grouping<TKey, TSource>(lastPartitionKey, itemsInPartition);
                }
            }
        }
    
        // it's a shame there's no ready-made public implementation that will do this
        private class Grouping<TKey, TSource> : IGrouping<TKey, TSource>
        {
            public Grouping(TKey key, List<TSource> items)
            {
                _items = items;
                Key = key;
            }
    
            public TKey Key { get; }
    
            public IEnumerator<TSource> GetEnumerator()
            {
                return _items.GetEnumerator();
            }
    
            IEnumerator IEnumerable.GetEnumerator()
            {
                return _items.GetEnumerator();
            }
    
            private readonly List<TSource> _items;
        }
    }
    
    0 讨论(0)
  • 2021-01-29 10:08

    Based on the comment clarifications (the question is really unclear now), I think this is what is needed.

    It uses an extension method that groups runs of keys together, GroupByRuns, that is based on a GroupByWhile the groups by testing consecutive items, which is based on ScanPair, which is a variation of my APL inspired Scan operator that is like Aggregate, but returns intermediate results, and uses a ValueTuple (Key, Value) to pair keys with values along the way.

    public static IEnumerable<IGrouping<int, TRes>> GroupByRuns<T, TKey, TRes>(this IEnumerable<T> src, Func<T,TKey> keySelector, Func<T,TRes> resultSelector, IEqualityComparer<TKey> cmp = null) {
        cmp = cmp ?? EqualityComparer<TKey>.Default;
        return src.GroupByWhile((prev, cur) => cmp.Equals(keySelector(prev), keySelector(cur)), resultSelector);
    }
    public static IEnumerable<IGrouping<int, T>> GroupByRuns<T, TKey>(this IEnumerable<T> src, Func<T,TKey> keySelector) => src.GroupByRuns(keySelector, e => e);
    public static IEnumerable<IGrouping<int, T>> GroupByRuns<T>(this IEnumerable<T> src) => src.GroupByRuns(e => e, e => e);
    
    public static IEnumerable<IGrouping<int, TRes>> GroupByWhile<T, TRes>(this IEnumerable<T> src, Func<T,T,bool> testFn, Func<T,TRes> 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<T, TKey>(this IEnumerable<T> 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;
            }
        }
    }
    

    I realize this is a lot of extension code, but by using the general ScanPair base, you can build other specialized grouping methods, such as GroupBySequential.

    Now you just GroupByRuns of Name and select the runs with more than one member, then convert each run to a List and the whole thing to a List:

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

    NOTE: For @Aominè, who had an interesting take on optimizing Count() > 1 using Take(2).Count() or @MichaelGunter using Skip(1).Any(), after GroupBy the sub-groups (internal type Grouping) each implement IList and the Count() method just gets the count directly from the Grouping.count field.

    0 讨论(0)
提交回复
热议问题